[
  {
    "path": ".dockerignore",
    "content": "# Node.js dependencies\nnode_modules\ninternalsite/node_modules\n\n# Go build artifacts and binaries\nbuild\ndist\n*.exe\nbeszel-agent\nbeszel_data*\npb_data\ndata\ntemp\n\n# Development and IDE files\n.vscode\n.idea*\n*.swc\n__debug_*\n\n# Git and version control\n.git\n.gitignore\n\n# Documentation and supplemental files\n*.md\nsupplemental\nfreebsd-port\n\n# Test files (exclude from production builds)\n*_test.go\ncoverage\n\n# Docker files\ndockerfile_*\n\n# Temporary files\n*.tmp\n*.bak\n*.log\n\n# OS specific files\n.DS_Store\nThumbs.db\n\n# .NET build artifacts\nagent/lhm/obj\nagent/lhm/bin\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.tsx linguist-language=Go"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Everything needs to be reviewed by Hank\n*   @henrygd"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/ideas.yml",
    "content": "body:\n  - type: dropdown\n    id: component\n    attributes:\n      label: Component\n      description: Which part of Beszel is this about?\n      options:\n        - Hub\n        - Agent\n        - Hub & Agent\n      default: 0\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Description\n      description: Please describe in detail what you want to share.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/support.yml",
    "content": "body:\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Welcome!\n      description: |\n        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:\n\n        Please note:\n        - For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/beszel).\n        **- Please do not submit support reqeusts that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.**\n\n      options:\n      - label: I have read the [Documentation](https://beszel.dev/guide/getting-started)\n        required: true\n      - label: I have checked the [Common Issues Guide](https://beszel.dev/guide/common-issues) and my problem was not mentioned there.\n        required: true\n      - label: I have searched open and closed issues and discussions and my problem was not mentioned before.\n        required: true\n      - label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/henrygd/beszel/releases).\n        required: true\n\n  - type: dropdown\n    id: component\n    attributes:\n      label: Component\n      description: Which part of Beszel is this about?\n      options:\n        - Hub\n        - Agent\n        - Hub & Agent\n    default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Problem Description\n      description: |\n        How to write a good bug report?\n\n        - Respect the issue template as much as possible.\n        - The title should be short and descriptive.\n        - Explain the conditions which led you to report this issue: the context.\n        - The context should lead to something, a problem that you’re facing.\n        - Remain clear and concise.\n        - 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)\n    validations:\n      required: true\n\n  - type: input\n    id: system\n    attributes:\n      label: OS / Architecture\n      placeholder: linux/amd64 (agent), freebsd/arm64 (hub)\n    validations:\n      required: true\n  \n#  - type: input\n#    id: version\n#    attributes:\n#      label: Beszel version\n#      placeholder: 0.9.1\n#    validations:\n#      required: true\n\n  - type: dropdown\n    id: install-method\n    attributes:\n      label: Installation method\n      options:\n        - Docker\n        - Binary\n        - Nix\n        - Unraid\n        - Coolify\n        - Other (please describe above)\n    validations:\n      required: true\n  \n  - type: textarea\n    id: config\n    attributes:\n      label: Configuration\n      description: Please provide any relevant service configuration\n      render: yaml\n\n  - type: textarea\n    id: hub-logs\n    attributes:\n      label: Hub Logs\n      description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).\n      render: json\n\n  - type: textarea\n    id: agent-logs\n    attributes:\n      label: Agent Logs\n      description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐛 Bug report\ndescription: Use this template to report a bug or issue.\ntitle: '[Bug]: '\nlabels: ['bug']\nbody:\n  - type: checkboxes\n    attributes:\n      label: Welcome!\n      description: |\n        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\n\n        Please note:\n        - For translation-related issues or requests, please use the [Crowdin project](https://crowdin.com/project/beszel).\n        - To request a change or feature, use the [feature request form](https://github.com/henrygd/beszel/issues/new?template=feature_request.yml).\n        - Any issues that can be resolved by consulting the documentation or by reviewing existing open or closed issues will be closed.\n        **- Please do not submit bugs that are specific to ZFS. We plan to add integration with ZFS utilities in the near future.**\n\n      options:\n      - label: I have read the [Documentation](https://beszel.dev/guide/getting-started)\n        required: true\n      - label: I have checked the [Common Issues Guide](https://beszel.dev/guide/common-issues) and my problem was not mentioned there.\n        required: true\n      - label: I have searched open and closed issues and my problem was not mentioned before.\n        required: true\n      - label: I have verified I am using the latest version available. You can check the latest release [here](https://github.com/henrygd/beszel/releases).\n        required: true\n\n  - type: dropdown\n    id: component\n    attributes:\n      label: Component\n      description: Which part of Beszel is this about?\n      options:\n        - Hub\n        - Agent\n        - Hub & Agent\n      default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Problem Description\n      description: |\n        How to write a good bug report?\n\n        - Respect the issue template as much as possible.\n        - The title should be short and descriptive.\n        - Explain the conditions which led you to report this issue: the context.\n        - The context should lead to something, a problem that you’re facing.\n        - Remain clear and concise.\n        - 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)\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: |\n        In a perfect world, what should have happened?\n        **Important:** Be specific. Vague descriptions like \"it should work\" are not helpful.\n      placeholder: When I got to the coffee pot, it should have been full.\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: Steps to Reproduce\n      description: |\n        Provide detailed, numbered steps that someone else can follow to reproduce the issue.\n        **Important:** Vague descriptions like \"it doesn't work\" or \"it's broken\" will result in the issue being closed.\n        Include specific actions, URLs, button clicks, and any relevant data or configuration.\n      placeholder: |\n        1. Go to the coffee pot.\n        2. Make more coffee.\n        3. Pour it into a cup.\n        4. Observe that the cup is empty instead of full.\n    validations:\n      required: true\n\n  - type: input\n    id: system\n    attributes:\n      label: OS / Architecture\n      placeholder: linux/amd64 (agent), freebsd/arm64 (hub)\n    validations:\n      required: true\n  \n  - type: input\n    id: version\n    attributes:\n      label: Beszel version\n      placeholder: 0.9.1\n    validations:\n      required: true\n  \n  - type: dropdown\n    id: install-method\n    attributes:\n      label: Installation method\n      options:\n        - Docker\n        - Binary\n        - Nix\n        - Unraid\n        - Coolify\n        - Other (please describe above)\n    validations:\n      required: true\n  \n  - type: textarea\n    id: config\n    attributes:\n      label: Configuration\n      description: Please provide any relevant service configuration\n      render: yaml\n  \n  - type: textarea\n    id: hub-logs\n    attributes:\n      label: Hub Logs\n      description: Check the logs page in PocketBase (`/_/#/logs`) for relevant errors (copy JSON).\n      render: json\n  \n  - type: textarea\n    id: agent-logs\n    attributes:\n      label: Agent Logs\n      description: Please provide any logs from the agent, if relevant. Use `LOG_LEVEL=debug` for more info.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 🗣️ Translations\n    url: https://crowdin.com/project/beszel\n    about: Please report translation issues and request new translations here. \n  - name: 💬 Support and questions\n    url: https://github.com/henrygd/beszel/discussions\n    about: Ask and answer questions here.\n  - name: ℹ️ View the Common Issues page\n    url: https://beszel.dev/guide/common-issues\n    about: Find information about commonly encountered problems.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 🚀 Feature request\ndescription: Request a new feature or change.\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: checkboxes\n    attributes:\n      label: Welcome!\n      description: |\n        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\n\n        Please note:\n        - For **Bug reports**, use the [Bug Form](https://github.com/henrygd/beszel/issues/new?template=bug_report.yml).\n        - Any requests for new translations should be requested within the [crowdin project](https://crowdin.com/project/beszel).\n        - Create one issue per feature request. This helps us keep track of requests and prioritize them accordingly.\n\n      options:\n      - label: I have searched open and closed feature requests to make sure this or similar feature request does not already exist.\n        required: true\n      - label: This is a feature request, not a bug report or support question.\n        required: true\n\n  - type: dropdown\n    id: component\n    attributes:\n      label: Component\n      description: Which part of Beszel is this about?\n      options:\n        - Hub\n        - Agent\n        - Hub & Agent\n      default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: |\n        Describe the solution or feature you'd like. Explain what problem this solves or what value it adds.\n        **Important:** Be specific and detailed. Vague requests like \"make it better\" will be closed.\n      placeholder: |\n        Example:\n        - What is the feature?\n        - What problem does it solve?\n        - How should it work?\n    validations:\n      required: true\n\n  - type: textarea\n    id: motivation\n    attributes:\n      label: Motivation / Use Case\n      description: Why do you want this feature? What problem does it solve?\n    validations:\n      required: true"
  },
  {
    "path": ".github/funding.yml",
    "content": "buy_me_a_coffee: henrygd\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## 📃 Description\n\nA 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.\n\n## 📖 Documentation\n\nAdd a link to the PR for [documentation](https://github.com/henrygd/beszel-docs) changes.\n\n## 🪵 Changelog\n\n### ➕ Added\n\n- one\n- two\n\n### ✏️ Changed\n\n- one\n- two\n\n### 🔧 Fixed\n\n- one\n- two\n\n### 🗑️ Removed\n\n- one\n- two\n\n## 📷 Screenshots\n\nIf this PR has any UI/UX changes it's strongly suggested you add screenshots here.\n"
  },
  {
    "path": ".github/workflows/docker-images.yml",
    "content": "name: Make docker images\n\non:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 5\n      matrix:\n        include:\n          # henrygd/beszel\n          - image: henrygd/beszel\n            dockerfile: ./internal/dockerfile_hub\n            registry: docker.io\n            username_secret: DOCKERHUB_USERNAME\n            password_secret: DOCKERHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n            \n          # henrygd/beszel-agent:alpine\n          - image: henrygd/beszel-agent\n            dockerfile: ./internal/dockerfile_agent_alpine\n            registry: docker.io\n            username_secret: DOCKERHUB_USERNAME\n            password_secret: DOCKERHUB_TOKEN\n            tags: |\n              type=raw,value=alpine\n              type=semver,pattern={{version}}-alpine\n              type=semver,pattern={{major}}.{{minor}}-alpine\n              type=semver,pattern={{major}}-alpine\n\n          # henrygd/beszel-agent-nvidia\n          - image: henrygd/beszel-agent-nvidia\n            dockerfile: ./internal/dockerfile_agent_nvidia\n            platforms: linux/amd64\n            registry: docker.io\n            username_secret: DOCKERHUB_USERNAME\n            password_secret: DOCKERHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # henrygd/beszel-agent-intel\n          - image: henrygd/beszel-agent-intel\n            dockerfile: ./internal/dockerfile_agent_intel\n            platforms: linux/amd64\n            registry: docker.io\n            username_secret: DOCKERHUB_USERNAME\n            password_secret: DOCKERHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # ghcr.io/henrygd/beszel\n          - image: ghcr.io/${{ github.repository }}/beszel\n            dockerfile: ./internal/dockerfile_hub\n            registry: ghcr.io\n            username: ${{ github.actor }}\n            password_secret: GITHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # ghcr.io/henrygd/beszel-agent\n          - image: ghcr.io/${{ github.repository }}/beszel-agent\n            dockerfile: ./internal/dockerfile_agent\n            registry: ghcr.io\n            username: ${{ github.actor }}\n            password_secret: GITHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=raw,value=latest\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # ghcr.io/henrygd/beszel-agent-nvidia\n          - image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia\n            dockerfile: ./internal/dockerfile_agent_nvidia\n            platforms: linux/amd64\n            registry: ghcr.io\n            username: ${{ github.actor }}\n            password_secret: GITHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # ghcr.io/henrygd/beszel-agent-intel\n          - image: ghcr.io/${{ github.repository }}/beszel-agent-intel\n            dockerfile: ./internal/dockerfile_agent_intel\n            platforms: linux/amd64\n            registry: ghcr.io\n            username: ${{ github.actor }}\n            password_secret: GITHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n          # ghcr.io/henrygd/beszel-agent:alpine\n          - image: ghcr.io/${{ github.repository }}/beszel-agent\n            dockerfile: ./internal/dockerfile_agent_alpine\n            registry: ghcr.io\n            username: ${{ github.actor }}\n            password_secret: GITHUB_TOKEN\n            tags: |\n              type=raw,value=alpine\n              type=semver,pattern={{version}}-alpine\n              type=semver,pattern={{major}}.{{minor}}-alpine\n              type=semver,pattern={{major}}-alpine\n\n          # henrygd/beszel-agent (keep at bottom so it gets built after :alpine and gets the latest tag)\n          - image: henrygd/beszel-agent\n            dockerfile: ./internal/dockerfile_agent\n            registry: docker.io\n            username_secret: DOCKERHUB_USERNAME\n            password_secret: DOCKERHUB_TOKEN\n            tags: |\n              type=raw,value=edge\n              type=semver,pattern={{version}}\n              type=semver,pattern={{major}}.{{minor}}\n              type=semver,pattern={{major}}\n              type=raw,value={{sha}},enable=${{ github.ref_type != 'tag' }}\n\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: bun install --no-save --cwd ./internal/site\n\n      - name: Build site\n        run: bun run --cwd ./internal/site build\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Docker metadata\n        id: metadata\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ matrix.image }}\n          tags: ${{ matrix.tags }}\n\n      # https://github.com/docker/login-action\n      - name: Login to Docker Hub\n        env:\n          password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}\n        if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'\n        uses: docker/login-action@v3\n        with:\n          username: ${{ matrix.username || secrets[matrix.username_secret] }}\n          password: ${{ secrets[matrix.password_secret] }}\n          registry: ${{ matrix.registry }}\n\n      # Build and push Docker image with Buildx (don't push on PR)\n      # https://github.com/docker/build-push-action\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: ./\n          file: ${{ matrix.dockerfile }}\n          platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}\n          push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}\n          tags: ${{ steps.metadata.outputs.tags }}\n          labels: ${{ steps.metadata.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/inactivity-actions.yml",
    "content": "name: 'Issue and PR Maintenance'\r\n\r\non:\r\n  schedule:\r\n    - cron: '0 0 * * *'   # runs at midnight UTC\r\n  workflow_dispatch:\r\n\r\npermissions:\r\n  actions: write\r\n  issues: write\r\n  pull-requests: write\r\n\r\njobs:\r\n  lock-inactive:\r\n    name: Lock Inactive Issues\r\n    runs-on: ubuntu-24.04\r\n    steps:\r\n      - uses: klaasnicolaas/action-inactivity-lock@v1.1.3\r\n        id: lock\r\n        with:\r\n          days-inactive-issues: 14\r\n          lock-reason-issues: \"\"\r\n          # Action can not skip PRs, set it to 100 years to cover it.\r\n          days-inactive-prs: 36524\r\n          lock-reason-prs: \"\"\r\n\r\n  close-stale:\r\n    name: Close Stale Issues\r\n    runs-on: ubuntu-24.04\r\n    steps:\r\n      - name: Close Stale Issues\r\n        uses: actions/stale@v10\r\n        with:\r\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\r\n\r\n          # Messaging\r\n          stale-issue-message: >\r\n            👋 This issue has been automatically marked as stale due to inactivity.\r\n            If this issue is still relevant, please comment to keep it open.\r\n            Without activity, it will be closed in 7 days.\r\n\r\n          close-issue-message: >\r\n            🔒 This issue has been automatically closed due to prolonged inactivity.\r\n            Feel free to open a new issue if you have further questions or concerns.\r\n\r\n          # Timing\r\n          days-before-issue-stale: 14\r\n          days-before-issue-close: 7\r\n          # Action can not skip PRs, set it to 100 years to cover it.\r\n          days-before-pr-stale: 36524\r\n\r\n          # Max issues to process before early exit. Next run resumes from cache. GH API limit: 5000.\r\n          operations-per-run: 1500\r\n\r\n          # Labels\r\n          stale-issue-label: 'stale'\r\n          remove-stale-when-updated: true\r\n          any-of-labels: 'awaiting-requester'\r\n          exempt-issue-labels: 'enhancement' \r\n\r\n          # Exemptions\r\n          exempt-assignees: true\r\n\r\n          exempt-milestones: true\r\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Make release and binaries\n\non:\n  push:\n    tags:\n      - \"v*\"\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Set up bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: bun install --no-save --cwd ./internal/site\n\n      - name: Build site\n        run: bun run --cwd ./internal/site build\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"^1.22.1\"\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: \"9.0.x\"\n\n      - name: Build .NET LHM executable for Windows sensors\n        run: |\n          dotnet build -c Release ./agent/lhm/beszel_lhm.csproj\n        shell: bash\n\n      - name: GoReleaser beszel\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          workdir: ./\n          distribution: goreleaser\n          version: latest\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.TOKEN || secrets.GITHUB_TOKEN }}\n          WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}\n          IS_FORK: ${{ github.repository_owner != 'henrygd' }}\n"
  },
  {
    "path": ".github/workflows/vulncheck.yml",
    "content": "# https://github.com/minio/minio/blob/master/.github/workflows/vulncheck.yml\n\nname: VulnCheck\non:\n  pull_request:\n    branches:\n      - main\n\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  vulncheck:\n    name: VulnCheck\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: 1.26.x\n          # cached: false\n      - name: Get official govulncheck\n        run: go install golang.org/x/vuln/cmd/govulncheck@latest\n        shell: bash\n      - name: Run govulncheck\n        run: govulncheck -show verbose ./...\n        shell: bash\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea.md\npb_data\ndata\ntemp\n.vscode\nbeszel-agent\nbeszel_data\nbeszel_data*\ndist\n*.exe\ninternal/cmd/hub/hub\ninternal/cmd/agent/agent\nagent.test\nnode_modules\nbuild\n*timestamp*\n.swc\ninternal/site/src/locales/**/*.ts\n*.bak\n__debug_*\nagent/lhm/obj\nagent/lhm/bin\ndockerfile_agent_dev\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nproject_name: beszel\n\nbefore:\n  hooks:\n    - go mod tidy\n    - go generate -run fetchsmartctl ./agent\n\nbuilds:\n  - id: beszel\n    binary: beszel\n    main: internal/cmd/hub/hub.go\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n      - windows\n      - freebsd\n    goarch:\n      - amd64\n      - arm64\n      - arm\n    ignore:\n      - goos: windows\n        goarch: arm64\n      - goos: windows\n        goarch: arm\n      - goos: freebsd\n        goarch: arm64\n      - goos: freebsd\n        goarch: arm\n\n  - id: beszel-agent\n    binary: beszel-agent\n    main: internal/cmd/agent/agent.go\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n      - freebsd\n      - openbsd\n      - windows\n    goarch:\n      - amd64\n      - arm64\n      - arm\n      - mips64\n      - riscv64\n      - mipsle\n      - mips\n      - ppc64le\n    gomips:\n      - hardfloat\n      - softfloat\n    ignore:\n      - goos: freebsd\n        goarch: arm\n      - goos: openbsd\n        goarch: arm\n      - goos: linux\n        goarch: mips64\n        gomips: softfloat\n      - goos: linux\n        goarch: mipsle\n        gomips: hardfloat\n      - goos: linux\n        goarch: mips\n        gomips: hardfloat\n      - goos: windows\n        goarch: arm\n      - goos: darwin\n        goarch: riscv64\n      - goos: windows\n        goarch: riscv64\n\n  - id: beszel-agent-linux-amd64-glibc\n    binary: beszel-agent\n    main: internal/cmd/agent/agent.go\n    env:\n      - CGO_ENABLED=0\n    flags:\n      - -tags=glibc\n    goos:\n      - linux\n    goarch:\n      - amd64\n\narchives:\n  - id: beszel-agent\n    formats: [tar.gz]\n    ids:\n      - beszel-agent\n    name_template: >-\n      {{ .Binary }}_\n      {{- .Os }}_\n      {{- .Arch }}\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n\n  - id: beszel-agent-linux-amd64-glibc\n    formats: [tar.gz]\n    ids:\n      - beszel-agent-linux-amd64-glibc\n    name_template: >-\n      {{ .Binary }}_\n      {{- .Os }}_\n      {{- .Arch }}_glibc\n\n  - id: beszel\n    formats: [tar.gz]\n    ids:\n      - beszel\n    name_template: >-\n      {{ .Binary }}_\n      {{- .Os }}_\n      {{- .Arch }}\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n\nnfpms:\n  - id: beszel-agent\n    package_name: beszel-agent\n    description: |-\n      Agent for Beszel\n      Beszel is a lightweight server monitoring platform that includes Docker\n      statistics, historical data, and alert functions. It has a friendly web\n      interface, simple configuration, and is ready to use out of the box.\n      It supports automatic backup, multi-user, OAuth authentication, and\n      API access.\n    maintainer: henrygd <hank@henrygd.me>\n    section: net\n    ids:\n      - beszel-agent\n    formats:\n      - deb\n    contents:\n      - src: ./supplemental/debian/beszel-agent.service\n        dst: lib/systemd/system/beszel-agent.service\n        packager: deb\n      - src: ./supplemental/debian/copyright\n        dst: usr/share/doc/beszel-agent/copyright\n        packager: deb\n      - src: ./supplemental/debian/lintian-overrides\n        dst: usr/share/lintian/overrides/beszel-agent\n        packager: deb\n    scripts:\n      postinstall: ./supplemental/debian/postinstall.sh\n      preremove: ./supplemental/debian/prerm.sh\n      postremove: ./supplemental/debian/postrm.sh\n    deb:\n      predepends:\n        - adduser\n        - debconf\n      scripts:\n        templates: ./supplemental/debian/templates\n        config: ./supplemental/debian/config.sh\n\nscoops:\n  - ids: [beszel-agent]\n    name: beszel-agent\n    repository:\n      owner: henrygd\n      name: beszel-scoops\n    homepage: \"https://beszel.dev\"\n    description: \"Agent for Beszel, a lightweight server monitoring platform.\"\n    license: MIT\n    skip_upload: '{{ if eq (tolower .Env.IS_FORK) \"true\" }}true{{ else }}auto{{ end }}'\n\n# # Needs choco installed, so doesn't build on linux / default gh workflow :(\n# chocolateys:\n#   - title: Beszel Agent\n#     ids: [beszel-agent]\n#     package_source_url: https://github.com/henrygd/beszel-chocolatey\n#     owners: henrygd\n#     authors: henrygd\n#     summary: 'Agent for Beszel, a lightweight server monitoring platform.'\n#     description: |\n#       Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.\n\n#       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.\n#     license_url: https://github.com/henrygd/beszel/blob/main/LICENSE\n#     project_url: https://beszel.dev\n#     project_source_url: https://github.com/henrygd/beszel\n#     docs_url: https://beszel.dev/guide/getting-started\n#     icon_url: https://cdn.jsdelivr.net/gh/selfhst/icons/png/beszel.png\n#     bug_tracker_url: https://github.com/henrygd/beszel/issues\n#     copyright: 2025 henrygd\n#     tags: foss cross-platform admin monitoring\n#     require_license_acceptance: false\n#     release_notes: 'https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}'\n\nbrews:\n  - ids: [beszel-agent]\n    name: beszel-agent\n    repository:\n      owner: henrygd\n      name: homebrew-beszel\n    homepage: \"https://beszel.dev\"\n    description: \"Agent for Beszel, a lightweight server monitoring platform.\"\n    license: MIT\n    skip_upload: '{{ if eq (tolower .Env.IS_FORK) \"true\" }}true{{ else }}auto{{ end }}'\n    extra_install: |\n      (bin/\"beszel-agent-launcher\").write <<~EOS\n        #!/bin/bash\n        set -a\n        if [ -f \"$HOME/.config/beszel/beszel-agent.env\" ]; then\n          source \"$HOME/.config/beszel/beszel-agent.env\"\n        fi\n        set +a\n        exec #{bin}/beszel-agent \"$@\"\n      EOS\n      (bin/\"beszel-agent-launcher\").chmod 0755\n    service: |\n      run [\"#{bin}/beszel-agent-launcher\"]\n      log_path \"#{Dir.home}/.cache/beszel/beszel-agent.log\"\n      error_log_path \"#{Dir.home}/.cache/beszel/beszel-agent.log\"\n      keep_alive true\n      restart_delay 5\n      process_type :background\n\nwinget:\n  - ids: [beszel-agent]\n    name: beszel-agent\n    package_identifier: henrygd.beszel-agent\n    publisher: henrygd\n    license: MIT\n    license_url: \"https://github.com/henrygd/beszel/blob/main/LICENSE\"\n    copyright: \"2025 henrygd\"\n    homepage: \"https://beszel.dev\"\n    release_notes_url: \"https://github.com/henrygd/beszel/releases/tag/v{{ .Version }}\"\n    publisher_support_url: \"https://github.com/henrygd/beszel/issues\"\n    short_description: \"Agent for Beszel, a lightweight server monitoring platform.\"\n    skip_upload: '{{ if eq (tolower .Env.IS_FORK) \"true\" }}true{{ else }}auto{{ end }}'\n    description: |\n      Beszel is a lightweight server monitoring platform that includes Docker\n      statistics, historical data, and alert functions. It has a friendly web\n      interface, simple configuration, and is ready to use out of the box.\n      It supports automatic backup, multi-user, OAuth authentication, and\n      API access.\n    tags:\n      - homelab\n      - monitoring\n      - self-hosted\n    repository:\n      owner: henrygd\n      name: beszel-winget\n      branch: henrygd.beszel-agent-{{ .Version }}\n      token: \"{{ .Env.WINGET_TOKEN }}\"\n      # pull_request:\n      #   enabled: true\n      #   draft: false\n      #   base:\n      #     owner: microsoft\n      #     name: winget-pkgs\n      #     branch: master\n\nrelease:\n  draft: true\n\nchangelog:\n  disable: true\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 henrygd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Default OS/ARCH values\nOS ?= $(shell go env GOOS)\nARCH ?= $(shell go env GOARCH)\n# Skip building the web UI if true\nSKIP_WEB ?= false\n# Controls NVML/glibc agent build tag behavior:\n# - auto (default): enable on linux/amd64 glibc hosts\n# - true: always enable\n# - false: always disable\nNVML ?= auto\n\n# Detect glibc host for local linux/amd64 builds.\nHOST_GLIBC := $(shell \\\n\tif [ \"$(OS)\" = \"linux\" ] && [ \"$(ARCH)\" = \"amd64\" ]; then \\\n\t\tfor 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 \\\n\t\t\t[ -e \"$$p\" ] && { echo true; exit 0; }; \\\n\t\tdone; \\\n\t\tif command -v ldd >/dev/null 2>&1; then \\\n\t\t\tif ldd --version 2>&1 | tr '[:upper:]' '[:lower:]' | awk '/gnu libc|glibc/{found=1} END{exit !found}'; then \\\n\t\t\t\techo true; \\\n\t\t\telse \\\n\t\t\t\techo false; \\\n\t\t\tfi; \\\n\t\telse \\\n\t\t\techo false; \\\n\t\tfi; \\\n\telse \\\n\t\techo false; \\\n\tfi)\n\n# Enable glibc build tag for NVML on supported Linux builds.\nAGENT_GO_TAGS :=\nifeq ($(NVML),true)\nAGENT_GO_TAGS := -tags glibc\nelse ifeq ($(NVML),auto)\nifeq ($(HOST_GLIBC),true)\nAGENT_GO_TAGS := -tags glibc\nendif\nendif\n\n# Set executable extension based on target OS\nEXE_EXT := $(if $(filter windows,$(OS)),.exe,)\n\n.PHONY: tidy build-agent build-hub build-hub-dev build clean lint dev-server dev-agent dev-hub dev generate-locales fetch-smartctl-conditional\n.DEFAULT_GOAL := build\n\nclean:\n\tgo clean\n\trm -rf ./build\n\nlint:\n\tgolangci-lint run\n\ntest:\n\tgo test -tags=testing ./...\n\ntidy:\n\tgo mod tidy\n\nbuild-web-ui:\n\t@if command -v bun >/dev/null 2>&1; then \\\n\t\tbun install --cwd ./internal/site && \\\n\t\tbun run --cwd ./internal/site build; \\\n\telse \\\n\t\tnpm install --prefix ./internal/site && \\\n\t\tnpm run --prefix ./internal/site build; \\\n\tfi\n\n# Conditional .NET build - only for Windows\nbuild-dotnet-conditional:\n\t@if [ \"$(OS)\" = \"windows\" ]; then \\\n\t\techo \"Building .NET executable for Windows...\"; \\\n\t\tif command -v dotnet >/dev/null 2>&1; then \\\n\t\t\trm -rf ./agent/lhm/bin; \\\n\t\t\tdotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \\\n\t\telse \\\n\t\t\techo \"Error: dotnet not found. Install .NET SDK to build Windows agent.\"; \\\n\t\t\texit 1; \\\n\t\tfi; \\\n\tfi\n\n# Download smartctl.exe at build time for Windows (skips if already present)\nfetch-smartctl-conditional:\n\t@if [ \"$(OS)\" = \"windows\" ]; then \\\n\t\tgo generate -run fetchsmartctl ./agent; \\\n\tfi\n\n# Update build-agent to include conditional .NET build\nbuild-agent: tidy build-dotnet-conditional fetch-smartctl-conditional\n\tGOOS=$(OS) GOARCH=$(ARCH) go build $(AGENT_GO_TAGS) -o ./build/beszel-agent_$(OS)_$(ARCH)$(EXE_EXT) -ldflags \"-w -s\" ./internal/cmd/agent\n\nbuild-hub: tidy $(if $(filter false,$(SKIP_WEB)),build-web-ui)\n\tGOOS=$(OS) GOARCH=$(ARCH) go build -o ./build/beszel_$(OS)_$(ARCH)$(EXE_EXT) -ldflags \"-w -s\" ./internal/cmd/hub\n\nbuild-hub-dev: tidy\n\tmkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html\n\tGOOS=$(OS) GOARCH=$(ARCH) go build -tags development -o ./build/beszel-dev_$(OS)_$(ARCH)$(EXE_EXT) -ldflags \"-w -s\" ./internal/cmd/hub\n\nbuild: build-agent build-hub\n\ngenerate-locales:\n\t@if [ ! -f ./internal/site/src/locales/en/en.ts ]; then \\\n\t\techo \"Generating locales...\"; \\\n\t\tcommand -v bun >/dev/null 2>&1 && cd ./internal/site && bun install && bun run sync || cd ./internal/site && npm install && npm run sync; \\\n\tfi\n\ndev-server: generate-locales\n\tcd ./internal/site\n\t@if command -v bun >/dev/null 2>&1; then \\\n\t\tcd ./internal/site && bun run dev --host 0.0.0.0; \\\n\telse \\\n\t\tcd ./internal/site && npm run dev --host 0.0.0.0; \\\n\tfi\n\ndev-hub: export ENV=dev\ndev-hub:\n\tmkdir -p ./internal/site/dist && touch ./internal/site/dist/index.html\n\t@if command -v entr >/dev/null 2>&1; then \\\n\t\tfind ./internal -type f -name '*.go' | entr -r -s \"cd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090\"; \\\n\telse \\\n\t\tcd ./internal/cmd/hub && go run -tags development . serve --http 0.0.0.0:8090; \\\n\tfi\n\ndev-agent:\n\t@if command -v entr >/dev/null 2>&1; then \\\n\t\tfind ./internal/cmd/agent/*.go ./agent/*.go | entr -r go run $(AGENT_GO_TAGS) github.com/henrygd/beszel/internal/cmd/agent; \\\n\telse \\\n\t\tgo run $(AGENT_GO_TAGS) github.com/henrygd/beszel/internal/cmd/agent; \\\n\tfi\n\t\nbuild-dotnet:\n\t@if command -v dotnet >/dev/null 2>&1; then \\\n\t\trm -rf ./agent/lhm/bin; \\\n\t\tdotnet build -c Release ./agent/lhm/beszel_lhm.csproj; \\\n\telse \\\n\t\techo \"dotnet not found\"; \\\n\tfi\n\n\n# KEY=\"...\" make -j dev\ndev: dev-server dev-hub dev-agent\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf you find a vulnerability in the latest version, please [submit a private advisory](https://github.com/henrygd/beszel/security/advisories/new).\n\nIf it's low severity (use best judgement) you may open an issue instead of an advisory.\n"
  },
  {
    "path": "agent/agent.go",
    "content": "// Package agent implements the Beszel monitoring agent that collects and serves system metrics.\n//\n// The agent runs on monitored systems and communicates collected data\n// to the Beszel hub for centralized monitoring and alerting.\npackage agent\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gliderlabs/ssh\"\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel/agent/deltatracker\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\tgossh \"golang.org/x/crypto/ssh\"\n)\n\ntype Agent struct {\n\tsync.Mutex                                                                      // Used to lock agent while collecting data\n\tdebug                     bool                                                  // true if LOG_LEVEL is set to debug\n\tzfs                       bool                                                  // true if system has arcstats\n\tmemCalc                   string                                                // Memory calculation formula\n\tfsNames                   []string                                              // List of filesystem device names being monitored\n\tfsStats                   map[string]*system.FsStats                            // Keeps track of disk stats for each filesystem\n\tdiskPrev                  map[uint16]map[string]prevDisk                        // Previous disk I/O counters per cache interval\n\tdiskUsageCacheDuration    time.Duration                                         // How long to cache disk usage (to avoid waking sleeping disks)\n\tlastDiskUsageUpdate       time.Time                                             // Last time disk usage was collected\n\tnetInterfaces             map[string]struct{}                                   // Stores all valid network interfaces\n\tnetIoStats                map[uint16]system.NetIoStats                          // Keeps track of bandwidth usage per cache interval\n\tnetInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers\n\tdockerManager             *dockerManager                                        // Manages Docker API requests\n\tsensorConfig              *SensorConfig                                         // Sensors config\n\tsystemInfo                system.Info                                           // Host system info (dynamic)\n\tsystemDetails             system.Details                                        // Host system details (static, once-per-connection)\n\tgpuManager                *GPUManager                                           // Manages GPU data\n\tcache                     *systemDataCache                                      // Cache for system stats based on cache time\n\tconnectionManager         *ConnectionManager                                    // Channel to signal connection events\n\thandlerRegistry           *HandlerRegistry                                      // Registry for routing incoming messages\n\tserver                    *ssh.Server                                           // SSH server\n\tdataDir                   string                                                // Directory for persisting data\n\tkeys                      []gossh.PublicKey                                     // SSH public keys\n\tsmartManager              *SmartManager                                         // Manages SMART data\n\tsystemdManager            *systemdManager                                       // Manages systemd services\n}\n\n// NewAgent creates a new agent with the given data directory for persisting data.\n// If the data directory is not set, it will attempt to find the optimal directory.\nfunc NewAgent(dataDir ...string) (agent *Agent, err error) {\n\tagent = &Agent{\n\t\tfsStats: make(map[string]*system.FsStats),\n\t\tcache:   NewSystemDataCache(),\n\t}\n\n\t// Initialize disk I/O previous counters storage\n\tagent.diskPrev = make(map[uint16]map[string]prevDisk)\n\t// Initialize per-cache-time network tracking structures\n\tagent.netIoStats = make(map[uint16]system.NetIoStats)\n\tagent.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])\n\n\tagent.dataDir, err = GetDataDir(dataDir...)\n\tif err != nil {\n\t\tslog.Warn(\"Data directory not found\")\n\t} else {\n\t\tslog.Info(\"Data directory\", \"path\", agent.dataDir)\n\t}\n\n\tagent.memCalc, _ = utils.GetEnv(\"MEM_CALC\")\n\tagent.sensorConfig = agent.newSensorConfig()\n\n\t// Parse disk usage cache duration (e.g., \"15m\", \"1h\") to avoid waking sleeping disks\n\tif diskUsageCache, exists := utils.GetEnv(\"DISK_USAGE_CACHE\"); exists {\n\t\tif duration, err := time.ParseDuration(diskUsageCache); err == nil {\n\t\t\tagent.diskUsageCacheDuration = duration\n\t\t\tslog.Info(\"DISK_USAGE_CACHE\", \"duration\", duration)\n\t\t} else {\n\t\t\tslog.Warn(\"Invalid DISK_USAGE_CACHE\", \"err\", err)\n\t\t}\n\t}\n\n\t// Set up slog with a log level determined by the LOG_LEVEL env var\n\tif logLevelStr, exists := utils.GetEnv(\"LOG_LEVEL\"); exists {\n\t\tswitch strings.ToLower(logLevelStr) {\n\t\tcase \"debug\":\n\t\t\tagent.debug = true\n\t\t\tslog.SetLogLoggerLevel(slog.LevelDebug)\n\t\tcase \"warn\":\n\t\t\tslog.SetLogLoggerLevel(slog.LevelWarn)\n\t\tcase \"error\":\n\t\t\tslog.SetLogLoggerLevel(slog.LevelError)\n\t\t}\n\t}\n\n\tslog.Debug(beszel.Version)\n\n\t// initialize docker manager\n\tagent.dockerManager = newDockerManager()\n\n\t// initialize system info\n\tagent.refreshSystemDetails()\n\n\t// SMART_INTERVAL env var to update smart data at this interval\n\tif smartIntervalEnv, exists := utils.GetEnv(\"SMART_INTERVAL\"); exists {\n\t\tif duration, err := time.ParseDuration(smartIntervalEnv); err == nil && duration > 0 {\n\t\t\tagent.systemDetails.SmartInterval = duration\n\t\t\tslog.Info(\"SMART_INTERVAL\", \"duration\", duration)\n\t\t} else {\n\t\t\tslog.Warn(\"Invalid SMART_INTERVAL\", \"err\", err)\n\t\t}\n\t}\n\n\t// initialize connection manager\n\tagent.connectionManager = newConnectionManager(agent)\n\n\t// initialize handler registry\n\tagent.handlerRegistry = NewHandlerRegistry()\n\n\t// initialize disk info\n\tagent.initializeDiskInfo()\n\n\t// initialize net io stats\n\tagent.initializeNetIoStats()\n\n\tagent.systemdManager, err = newSystemdManager()\n\tif err != nil {\n\t\tslog.Debug(\"Systemd\", \"err\", err)\n\t}\n\n\tagent.smartManager, err = NewSmartManager()\n\tif err != nil {\n\t\tslog.Debug(\"SMART\", \"err\", err)\n\t}\n\n\t// initialize GPU manager\n\tagent.gpuManager, err = NewGPUManager()\n\tif err != nil {\n\t\tslog.Debug(\"GPU\", \"err\", err)\n\t}\n\n\t// if debugging, print stats\n\tif agent.debug {\n\t\tslog.Debug(\"Stats\", \"data\", agent.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000, IncludeDetails: true}))\n\t}\n\n\treturn agent, nil\n}\n\nfunc (a *Agent) gatherStats(options common.DataRequestOptions) *system.CombinedData {\n\ta.Lock()\n\tdefer a.Unlock()\n\n\tcacheTimeMs := options.CacheTimeMs\n\tdata, isCached := a.cache.Get(cacheTimeMs)\n\tif isCached {\n\t\tslog.Debug(\"Cached data\", \"cacheTimeMs\", cacheTimeMs)\n\t\treturn data\n\t}\n\n\t*data = system.CombinedData{\n\t\tStats: a.getSystemStats(cacheTimeMs),\n\t\tInfo:  a.systemInfo,\n\t}\n\n\t// Include static system details only when requested\n\tif options.IncludeDetails {\n\t\tdata.Details = &a.systemDetails\n\t}\n\n\t// slog.Info(\"System data\", \"data\", data, \"cacheTimeMs\", cacheTimeMs)\n\n\tif a.dockerManager != nil {\n\t\tif containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {\n\t\t\tdata.Containers = containerStats\n\t\t\tslog.Debug(\"Containers\", \"data\", data.Containers)\n\t\t} else {\n\t\t\tslog.Debug(\"Containers\", \"err\", err)\n\t\t}\n\t}\n\n\t// skip updating systemd services if cache time is not the default 60sec interval\n\tif a.systemdManager != nil && cacheTimeMs == 60_000 {\n\t\ttotalCount := uint16(a.systemdManager.getServiceStatsCount())\n\t\tif totalCount > 0 {\n\t\t\tnumFailed := a.systemdManager.getFailedServiceCount()\n\t\t\tdata.Info.Services = []uint16{totalCount, numFailed}\n\t\t}\n\t\tif a.systemdManager.hasFreshStats {\n\t\t\tdata.SystemdServices = a.systemdManager.getServiceStats(nil, false)\n\t\t}\n\t}\n\n\tdata.Stats.ExtraFs = make(map[string]*system.FsStats)\n\tdata.Info.ExtraFsPct = make(map[string]float64)\n\tfor name, stats := range a.fsStats {\n\t\tif !stats.Root && stats.DiskTotal > 0 {\n\t\t\t// Use custom name if available, otherwise use device name\n\t\t\tkey := name\n\t\t\tif stats.Name != \"\" {\n\t\t\t\tkey = stats.Name\n\t\t\t}\n\t\t\tdata.Stats.ExtraFs[key] = stats\n\t\t\t// Add percentages to Info struct for dashboard\n\t\t\tif stats.DiskTotal > 0 {\n\t\t\t\tpct := utils.TwoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)\n\t\t\t\tdata.Info.ExtraFsPct[key] = pct\n\t\t\t}\n\t\t}\n\t}\n\tslog.Debug(\"Extra FS\", \"data\", data.Stats.ExtraFs)\n\n\ta.cache.Set(data, cacheTimeMs)\n\treturn data\n}\n\n// Start initializes and starts the agent with optional WebSocket connection\nfunc (a *Agent) Start(serverOptions ServerOptions) error {\n\ta.keys = serverOptions.Keys\n\treturn a.connectionManager.Start(serverOptions)\n}\n\nfunc (a *Agent) getFingerprint() string {\n\treturn GetFingerprint(a.dataDir, a.systemDetails.Hostname, a.systemDetails.CpuModel)\n}\n"
  },
  {
    "path": "agent/agent_cache.go",
    "content": "package agent\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\ntype systemDataCache struct {\n\tsync.RWMutex\n\tcache map[uint16]*cacheNode\n}\n\ntype cacheNode struct {\n\tdata       *system.CombinedData\n\tlastUpdate time.Time\n}\n\n// NewSystemDataCache creates a cache keyed by the polling interval in milliseconds.\nfunc NewSystemDataCache() *systemDataCache {\n\treturn &systemDataCache{\n\t\tcache: make(map[uint16]*cacheNode),\n\t}\n}\n\n// Get returns cached combined data when the entry is still considered fresh.\nfunc (c *systemDataCache) Get(cacheTimeMs uint16) (stats *system.CombinedData, isCached bool) {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tnode, ok := c.cache[cacheTimeMs]\n\tif !ok {\n\t\treturn &system.CombinedData{}, false\n\t}\n\t// allowedSkew := time.Second\n\t// isFresh := time.Since(node.lastUpdate) < time.Duration(cacheTimeMs)*time.Millisecond-allowedSkew\n\t// allow a 50% skew of the cache time\n\tisFresh := time.Since(node.lastUpdate) < time.Duration(cacheTimeMs/2)*time.Millisecond\n\treturn node.data, isFresh\n}\n\n// Set stores the latest combined data snapshot for the given interval.\nfunc (c *systemDataCache) Set(data *system.CombinedData, cacheTimeMs uint16) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tnode, ok := c.cache[cacheTimeMs]\n\tif !ok {\n\t\tnode = &cacheNode{}\n\t\tc.cache[cacheTimeMs] = node\n\t}\n\tnode.data = data\n\tnode.lastUpdate = time.Now()\n}\n"
  },
  {
    "path": "agent/agent_cache_test.go",
    "content": "//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/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createTestCacheData() *system.CombinedData {\n\treturn &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tCpu:       50.5,\n\t\t\tMem:       8192,\n\t\t\tDiskTotal: 100000,\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t},\n\t\tContainers: []*container.Stats{\n\t\t\t{\n\t\t\t\tName: \"test-container\",\n\t\t\t\tCpu:  25.0,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestNewSystemDataCache(t *testing.T) {\n\tcache := NewSystemDataCache()\n\trequire.NotNil(t, cache)\n\tassert.NotNil(t, cache.cache)\n\tassert.Empty(t, cache.cache)\n}\n\nfunc TestCacheGetSet(t *testing.T) {\n\tcache := NewSystemDataCache()\n\tdata := createTestCacheData()\n\n\t// Test setting data\n\tcache.Set(data, 1000) // 1 second cache\n\n\t// Test getting fresh data\n\tretrieved, isCached := cache.Get(1000)\n\tassert.True(t, isCached)\n\tassert.Equal(t, data, retrieved)\n\n\t// Test getting non-existent cache key\n\t_, isCached = cache.Get(2000)\n\tassert.False(t, isCached)\n}\n\nfunc TestCacheFreshness(t *testing.T) {\n\tcache := NewSystemDataCache()\n\tdata := createTestCacheData()\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tcacheTimeMs uint16\n\t\tsleepMs     time.Duration\n\t\texpectFresh bool\n\t}{\n\t\t{\n\t\t\tname:        \"fresh data - well within cache time\",\n\t\t\tcacheTimeMs: 1000, // 1 second\n\t\t\tsleepMs:     100,  // 100ms\n\t\t\texpectFresh: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"fresh data - at 50% of cache time boundary\",\n\t\t\tcacheTimeMs: 1000, // 1 second, 50% = 500ms\n\t\t\tsleepMs:     499,  // just under 500ms\n\t\t\texpectFresh: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"stale data - exactly at 50% cache time\",\n\t\t\tcacheTimeMs: 1000, // 1 second, 50% = 500ms\n\t\t\tsleepMs:     500,  // exactly 500ms\n\t\t\texpectFresh: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"stale data - well beyond cache time\",\n\t\t\tcacheTimeMs: 1000, // 1 second\n\t\t\tsleepMs:     800,  // 800ms\n\t\t\texpectFresh: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"short cache time\",\n\t\t\tcacheTimeMs: 200, // 200ms, 50% = 100ms\n\t\t\tsleepMs:     150, // 150ms > 100ms\n\t\t\texpectFresh: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\t// Set data\n\t\t\t\tcache.Set(data, tc.cacheTimeMs)\n\n\t\t\t\t// Wait for the specified duration\n\t\t\t\tif tc.sleepMs > 0 {\n\t\t\t\t\ttime.Sleep(tc.sleepMs * time.Millisecond)\n\t\t\t\t}\n\n\t\t\t\t// Check freshness\n\t\t\t\t_, isCached := cache.Get(tc.cacheTimeMs)\n\t\t\t\tassert.Equal(t, tc.expectFresh, isCached)\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestCacheMultipleIntervals(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tcache := NewSystemDataCache()\n\t\tdata1 := createTestCacheData()\n\t\tdata2 := &system.CombinedData{\n\t\t\tStats: system.Stats{\n\t\t\t\tCpu: 75.0,\n\t\t\t\tMem: 16384,\n\t\t\t},\n\t\t\tInfo: system.Info{\n\t\t\t\tAgentVersion: \"0.12.0\",\n\t\t\t},\n\t\t\tContainers: []*container.Stats{},\n\t\t}\n\n\t\t// Set data for different intervals\n\t\tcache.Set(data1, 500)  // 500ms cache\n\t\tcache.Set(data2, 1000) // 1000ms cache\n\n\t\t// Both should be fresh immediately\n\t\tretrieved1, isCached1 := cache.Get(500)\n\t\tassert.True(t, isCached1)\n\t\tassert.Equal(t, data1, retrieved1)\n\n\t\tretrieved2, isCached2 := cache.Get(1000)\n\t\tassert.True(t, isCached2)\n\t\tassert.Equal(t, data2, retrieved2)\n\n\t\t// Wait 300ms - 500ms cache should be stale (250ms threshold), 1000ms should still be fresh (500ms threshold)\n\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t_, isCached1 = cache.Get(500)\n\t\tassert.False(t, isCached1)\n\n\t\t_, isCached2 = cache.Get(1000)\n\t\tassert.True(t, isCached2)\n\n\t\t// Wait another 300ms (total 600ms) - now 1000ms cache should also be stale\n\t\ttime.Sleep(300 * time.Millisecond)\n\t\t_, isCached2 = cache.Get(1000)\n\t\tassert.False(t, isCached2)\n\t})\n}\n\nfunc TestCacheOverwrite(t *testing.T) {\n\tcache := NewSystemDataCache()\n\tdata1 := createTestCacheData()\n\tdata2 := &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tCpu: 90.0,\n\t\t\tMem: 32768,\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t},\n\t\tContainers: []*container.Stats{},\n\t}\n\n\t// Set initial data\n\tcache.Set(data1, 1000)\n\tretrieved, isCached := cache.Get(1000)\n\tassert.True(t, isCached)\n\tassert.Equal(t, data1, retrieved)\n\n\t// Overwrite with new data\n\tcache.Set(data2, 1000)\n\tretrieved, isCached = cache.Get(1000)\n\tassert.True(t, isCached)\n\tassert.Equal(t, data2, retrieved)\n\tassert.NotEqual(t, data1, retrieved)\n}\n\nfunc TestCacheMiss(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tcache := NewSystemDataCache()\n\n\t\t// Test getting from empty cache\n\t\t_, isCached := cache.Get(1000)\n\t\tassert.False(t, isCached)\n\n\t\t// Set data for one interval\n\t\tdata := createTestCacheData()\n\t\tcache.Set(data, 1000)\n\n\t\t// Test getting different interval\n\t\t_, isCached = cache.Get(2000)\n\t\tassert.False(t, isCached)\n\n\t\t// Test getting after data has expired\n\t\ttime.Sleep(600 * time.Millisecond) // 600ms > 500ms (50% of 1000ms)\n\t\t_, isCached = cache.Get(1000)\n\t\tassert.False(t, isCached)\n\t})\n}\n\nfunc TestCacheZeroInterval(t *testing.T) {\n\tcache := NewSystemDataCache()\n\tdata := createTestCacheData()\n\n\t// Set with zero interval - should allow immediate cache\n\tcache.Set(data, 0)\n\n\t// With 0 interval, 50% is 0, so it should never be considered fresh\n\t// (time.Since(lastUpdate) >= 0, which is not < 0)\n\t_, isCached := cache.Get(0)\n\tassert.False(t, isCached)\n}\n\nfunc TestCacheLargeInterval(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tcache := NewSystemDataCache()\n\t\tdata := createTestCacheData()\n\n\t\t// Test with maximum uint16 value\n\t\tcache.Set(data, 65535) // ~65 seconds\n\n\t\t// Should be fresh immediately\n\t\t_, isCached := cache.Get(65535)\n\t\tassert.True(t, isCached)\n\n\t\t// Should still be fresh after a short time\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\t_, isCached = cache.Get(65535)\n\t\tassert.True(t, isCached)\n\t})\n}\n"
  },
  {
    "path": "agent/agent_test_helpers.go",
    "content": "//go:build testing\n\npackage agent\n\n// TESTING ONLY: GetConnectionManager is a helper function to get the connection manager for testing.\nfunc (a *Agent) GetConnectionManager() *ConnectionManager {\n\treturn a.connectionManager\n}\n"
  },
  {
    "path": "agent/battery/battery.go",
    "content": "//go:build !freebsd\n\n// Package battery provides functions to check if the system has a battery and to get the battery stats.\npackage battery\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"math\"\n\n\t\"github.com/distatus/battery\"\n)\n\nvar (\n\tsystemHasBattery   = false\n\thaveCheckedBattery = false\n)\n\n// HasReadableBattery checks if the system has a battery and returns true if it does.\nfunc HasReadableBattery() bool {\n\tif haveCheckedBattery {\n\t\treturn systemHasBattery\n\t}\n\thaveCheckedBattery = true\n\tbatteries, err := battery.GetAll()\n\tfor _, bat := range batteries {\n\t\tif bat != nil && (bat.Full > 0 || bat.Design > 0) {\n\t\t\tsystemHasBattery = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !systemHasBattery {\n\t\tslog.Debug(\"No battery found\", \"err\", err)\n\t}\n\treturn systemHasBattery\n}\n\n// GetBatteryStats returns the current battery percent and charge state\n// percent = (current charge of all batteries) / (sum of designed/full capacity of all batteries)\nfunc GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {\n\tif !HasReadableBattery() {\n\t\treturn batteryPercent, batteryState, errors.ErrUnsupported\n\t}\n\tbatteries, err := battery.GetAll()\n\t// we'll handle errors later by skipping batteries with errors, rather\n\t// than skipping everything because of the presence of some errors.\n\tif len(batteries) == 0 {\n\t\treturn batteryPercent, batteryState, errors.New(\"no batteries\")\n\t}\n\n\ttotalCapacity := float64(0)\n\ttotalCharge := float64(0)\n\terrs, partialErrs := err.(battery.Errors)\n\n\tbatteryState = math.MaxUint8\n\n\tfor i, bat := range batteries {\n\t\tif partialErrs && errs[i] != nil {\n\t\t\t// if there were some errors, like missing data, skip it\n\t\t\tcontinue\n\t\t}\n\t\tif bat == nil || bat.Full == 0 {\n\t\t\t// skip batteries with no capacity. Charge is unlikely to ever be zero, but\n\t\t\t// we can't guarantee that, so don't skip based on charge.\n\t\t\tcontinue\n\t\t}\n\t\ttotalCapacity += bat.Full\n\t\ttotalCharge += min(bat.Current, bat.Full)\n\t\tif bat.State.Raw >= 0 {\n\t\t\tbatteryState = uint8(bat.State.Raw)\n\t\t}\n\t}\n\n\tif totalCapacity == 0 || batteryState == math.MaxUint8 {\n\t\t// for macs there's sometimes a ghost battery with 0 capacity\n\t\t// https://github.com/distatus/battery/issues/34\n\t\t// Instead of skipping over those batteries, we'll check for total 0 capacity\n\t\t// and return an error. This also prevents a divide by zero.\n\t\treturn batteryPercent, batteryState, errors.New(\"no battery capacity\")\n\t}\n\n\tbatteryPercent = uint8(totalCharge / totalCapacity * 100)\n\treturn batteryPercent, batteryState, nil\n}\n"
  },
  {
    "path": "agent/battery/battery_freebsd.go",
    "content": "//go:build freebsd\n\npackage battery\n\nimport \"errors\"\n\nfunc HasReadableBattery() bool {\n\treturn false\n}\n\nfunc GetBatteryStats() (uint8, uint8, error) {\n\treturn 0, 0, errors.ErrUnsupported\n}\n"
  },
  {
    "path": "agent/client.go",
    "content": "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\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/lxzan/gws\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nconst (\n\twsDeadline = 70 * time.Second\n)\n\n// WebSocketClient manages the WebSocket connection between the agent and hub.\n// It handles authentication, message routing, and connection lifecycle management.\ntype WebSocketClient struct {\n\tgws.BuiltinEventHandler\n\toptions            *gws.ClientOption                   // WebSocket client configuration options\n\tagent              *Agent                              // Reference to the parent agent\n\tConn               *gws.Conn                           // Active WebSocket connection\n\thubURL             *url.URL                            // Parsed hub URL for connection\n\ttoken              string                              // Authentication token for hub registration\n\tfingerprint        string                              // System fingerprint for identification\n\thubRequest         *common.HubRequest[cbor.RawMessage] // Reusable request structure for message parsing\n\tlastConnectAttempt time.Time                           // Timestamp of last connection attempt\n\thubVerified        bool                                // Whether the hub has been cryptographically verified\n}\n\n// newWebSocketClient creates a new WebSocket client for the given agent.\n// It reads configuration from environment variables and validates the hub URL.\nfunc newWebSocketClient(agent *Agent) (client *WebSocketClient, err error) {\n\thubURLStr, exists := utils.GetEnv(\"HUB_URL\")\n\tif !exists {\n\t\treturn nil, errors.New(\"HUB_URL environment variable not set\")\n\t}\n\n\tclient = &WebSocketClient{}\n\n\tclient.hubURL, err = url.Parse(hubURLStr)\n\tif err != nil {\n\t\treturn nil, errors.New(\"invalid hub URL\")\n\t}\n\t// get registration token\n\tclient.token, err = getToken()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.agent = agent\n\tclient.hubRequest = &common.HubRequest[cbor.RawMessage]{}\n\tclient.fingerprint = agent.getFingerprint()\n\n\treturn client, nil\n}\n\n// getToken returns the token for the WebSocket client.\n// It first checks the TOKEN environment variable, then the TOKEN_FILE environment variable.\n// If neither is set, it returns an error.\nfunc getToken() (string, error) {\n\t// get token from env var\n\ttoken, _ := utils.GetEnv(\"TOKEN\")\n\tif token != \"\" {\n\t\treturn token, nil\n\t}\n\t// get token from file\n\ttokenFile, _ := utils.GetEnv(\"TOKEN_FILE\")\n\tif tokenFile == \"\" {\n\t\treturn \"\", errors.New(\"must set TOKEN or TOKEN_FILE\")\n\t}\n\ttokenBytes, err := os.ReadFile(tokenFile)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(tokenBytes)), nil\n}\n\n// getOptions returns the WebSocket client options, creating them if necessary.\n// It configures the connection URL, TLS settings, and authentication headers.\nfunc (client *WebSocketClient) getOptions() *gws.ClientOption {\n\tif client.options != nil {\n\t\treturn client.options\n\t}\n\n\t// update the hub url to use websocket scheme and api path\n\tif client.hubURL.Scheme == \"https\" {\n\t\tclient.hubURL.Scheme = \"wss\"\n\t} else {\n\t\tclient.hubURL.Scheme = \"ws\"\n\t}\n\tclient.hubURL.Path = path.Join(client.hubURL.Path, \"api/beszel/agent-connect\")\n\n\tclient.options = &gws.ClientOption{\n\t\tAddr:      client.hubURL.String(),\n\t\tTlsConfig: &tls.Config{InsecureSkipVerify: true},\n\t\tRequestHeader: http.Header{\n\t\t\t\"User-Agent\": []string{getUserAgent()},\n\t\t\t\"X-Token\":    []string{client.token},\n\t\t\t\"X-Beszel\":   []string{beszel.Version},\n\t\t},\n\t}\n\treturn client.options\n}\n\n// Connect establishes a WebSocket connection to the hub.\n// It closes any existing connection before attempting to reconnect.\nfunc (client *WebSocketClient) Connect() (err error) {\n\tclient.lastConnectAttempt = time.Now()\n\n\t// make sure previous connection is closed\n\tclient.Close()\n\n\tclient.Conn, _, err = gws.NewClient(client, client.getOptions())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo client.Conn.ReadLoop()\n\n\treturn nil\n}\n\n// OnOpen handles WebSocket connection establishment.\n// It sets a deadline for the connection to prevent hanging.\nfunc (client *WebSocketClient) OnOpen(conn *gws.Conn) {\n\tconn.SetDeadline(time.Now().Add(wsDeadline))\n}\n\n// OnClose handles WebSocket connection closure.\n// It logs the closure reason and notifies the connection manager.\nfunc (client *WebSocketClient) OnClose(conn *gws.Conn, err error) {\n\tif err != nil {\n\t\tslog.Warn(\"Connection closed\", \"err\", strings.TrimPrefix(err.Error(), \"gws: \"))\n\t}\n\tclient.agent.connectionManager.eventChan <- WebSocketDisconnect\n}\n\n// OnMessage handles incoming WebSocket messages from the hub.\n// It decodes CBOR messages and routes them to appropriate handlers.\nfunc (client *WebSocketClient) OnMessage(conn *gws.Conn, message *gws.Message) {\n\tdefer message.Close()\n\tconn.SetDeadline(time.Now().Add(wsDeadline))\n\n\tif message.Opcode != gws.OpcodeBinary {\n\t\treturn\n\t}\n\n\tvar HubRequest common.HubRequest[cbor.RawMessage]\n\n\terr := cbor.Unmarshal(message.Data.Bytes(), &HubRequest)\n\tif err != nil {\n\t\tslog.Error(\"Error parsing message\", \"err\", err)\n\t\treturn\n\t}\n\n\tif err := client.handleHubRequest(&HubRequest, HubRequest.Id); err != nil {\n\t\tslog.Error(\"Error handling message\", \"err\", err)\n\t}\n}\n\n// OnPing handles WebSocket ping frames.\n// It responds with a pong and updates the connection deadline.\nfunc (client *WebSocketClient) OnPing(conn *gws.Conn, message []byte) {\n\tconn.SetDeadline(time.Now().Add(wsDeadline))\n\tconn.WritePong(message)\n}\n\n// handleAuthChallenge verifies the authenticity of the hub and returns the system's fingerprint.\nfunc (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.RawMessage], requestID *uint32) (err error) {\n\tvar authRequest common.FingerprintRequest\n\tif err := cbor.Unmarshal(msg.Data, &authRequest); err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.verifySignature(authRequest.Signature); err != nil {\n\t\treturn err\n\t}\n\n\tclient.hubVerified = true\n\tclient.agent.connectionManager.eventChan <- WebSocketConnect\n\n\tresponse := &common.FingerprintResponse{\n\t\tFingerprint: client.fingerprint,\n\t}\n\n\tif authRequest.NeedSysInfo {\n\t\tresponse.Name, _ = utils.GetEnv(\"SYSTEM_NAME\")\n\t\tresponse.Hostname = client.agent.systemDetails.Hostname\n\t\tserverAddr := client.agent.connectionManager.serverOptions.Addr\n\t\t_, response.Port, _ = net.SplitHostPort(serverAddr)\n\t}\n\n\treturn client.sendResponse(response, requestID)\n}\n\n// verifySignature verifies the signature of the token using the public keys.\nfunc (client *WebSocketClient) verifySignature(signature []byte) (err error) {\n\tfor _, pubKey := range client.agent.keys {\n\t\tsig := ssh.Signature{\n\t\t\tFormat: pubKey.Type(),\n\t\t\tBlob:   signature,\n\t\t}\n\t\tif err = pubKey.Verify([]byte(client.token), &sig); err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errors.New(\"invalid signature - check KEY value\")\n}\n\n// Close closes the WebSocket connection gracefully.\n// This method is safe to call multiple times.\nfunc (client *WebSocketClient) Close() {\n\tif client.Conn != nil {\n\t\t_ = client.Conn.WriteClose(1000, nil)\n\t}\n}\n\n// handleHubRequest routes the request to the appropriate handler using the handler registry.\nfunc (client *WebSocketClient) handleHubRequest(msg *common.HubRequest[cbor.RawMessage], requestID *uint32) error {\n\tctx := &HandlerContext{\n\t\tClient:       client,\n\t\tAgent:        client.agent,\n\t\tRequest:      msg,\n\t\tRequestID:    requestID,\n\t\tHubVerified:  client.hubVerified,\n\t\tSendResponse: client.sendResponse,\n\t}\n\treturn client.agent.handlerRegistry.Handle(ctx)\n}\n\n// sendMessage encodes the given data to CBOR and sends it as a binary message over the WebSocket connection to the hub.\nfunc (client *WebSocketClient) sendMessage(data any) error {\n\tbytes, err := cbor.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = client.Conn.WriteMessage(gws.OpcodeBinary, bytes)\n\tif err != nil {\n\t\t// If writing fails (e.g., broken pipe due to network issues),\n\t\t// close the connection to trigger reconnection logic (#1263)\n\t\tclient.Close()\n\t}\n\treturn err\n}\n\n// sendResponse sends a response with optional request ID.\n// For ID-based requests, we must populate legacy typed fields for backward\n// compatibility with older hubs (<= 0.17) that don't read the generic Data field.\nfunc (client *WebSocketClient) sendResponse(data any, requestID *uint32) error {\n\tif requestID != nil {\n\t\tresponse := newAgentResponse(data, requestID)\n\t\treturn client.sendMessage(response)\n\t}\n\t// Legacy format - send data directly\n\treturn client.sendMessage(data)\n}\n\n// getUserAgent returns one of two User-Agent strings based on current time.\n// This is used to avoid being blocked by Cloudflare or other anti-bot measures.\nfunc getUserAgent() string {\n\tconst (\n\t\tuaBase    = \"Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\"\n\t\tuaWindows = \"Windows NT 11.0; Win64; x64\"\n\t\tuaMac     = \"Macintosh; Intel Mac OS X 14_0_0\"\n\t)\n\tif time.Now().UnixNano()%2 == 0 {\n\t\treturn fmt.Sprintf(uaBase, uaWindows)\n\t}\n\treturn fmt.Sprintf(uaBase, uaMac)\n}\n"
  },
  {
    "path": "agent/client_test.go",
    "content": "//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.com/henrygd/beszel\"\n\n\t\"github.com/henrygd/beszel/internal/common\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// TestNewWebSocketClient tests WebSocket client creation\nfunc TestNewWebSocketClient(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\thubURL      string\n\t\ttoken       string\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname:        \"valid configuration\",\n\t\t\thubURL:      \"http://localhost:8080\",\n\t\t\ttoken:       \"test-token-123\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid https URL\",\n\t\t\thubURL:      \"https://hub.example.com\",\n\t\t\ttoken:       \"secure-token\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"missing hub URL\",\n\t\t\thubURL:      \"\",\n\t\t\ttoken:       \"test-token\",\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"HUB_URL environment variable not set\",\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid URL\",\n\t\t\thubURL:      \"ht\\ttp://invalid\",\n\t\t\ttoken:       \"test-token\",\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"invalid hub URL\",\n\t\t},\n\t\t{\n\t\t\tname:        \"missing token\",\n\t\t\thubURL:      \"http://localhost:8080\",\n\t\t\ttoken:       \"\",\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"must set TOKEN or TOKEN_FILE\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Set up environment\n\t\t\tif tc.hubURL != \"\" {\n\t\t\t\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", tc.hubURL)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\t\t}\n\t\t\tif tc.token != \"\" {\n\t\t\t\tos.Setenv(\"BESZEL_AGENT_TOKEN\", tc.token)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\t\t}()\n\n\t\t\tclient, err := newWebSocketClient(agent)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tif err != nil && tc.errorMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tc.errorMsg)\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, client)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, client)\n\t\t\t\tassert.Equal(t, agent, client.agent)\n\t\t\t\tassert.Equal(t, tc.token, client.token)\n\t\t\t\tassert.Equal(t, tc.hubURL, client.hubURL.String())\n\t\t\t\tassert.NotEmpty(t, client.fingerprint)\n\t\t\t\tassert.NotNil(t, client.hubRequest)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWebSocketClient_GetOptions tests WebSocket client options configuration\nfunc TestWebSocketClient_GetOptions(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tinputURL       string\n\t\texpectedScheme string\n\t\texpectedPath   string\n\t}{\n\t\t{\n\t\t\tname:           \"http to ws conversion\",\n\t\t\tinputURL:       \"http://localhost:8080\",\n\t\t\texpectedScheme: \"ws\",\n\t\t\texpectedPath:   \"/api/beszel/agent-connect\",\n\t\t},\n\t\t{\n\t\t\tname:           \"https to wss conversion\",\n\t\t\tinputURL:       \"https://hub.example.com\",\n\t\t\texpectedScheme: \"wss\",\n\t\t\texpectedPath:   \"/api/beszel/agent-connect\",\n\t\t},\n\t\t{\n\t\t\tname:           \"existing path preservation\",\n\t\t\tinputURL:       \"http://localhost:8080/custom/path\",\n\t\t\texpectedScheme: \"ws\",\n\t\t\texpectedPath:   \"/custom/path/api/beszel/agent-connect\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Set up environment\n\t\t\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", tc.inputURL)\n\t\t\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\t\t\tdefer func() {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\t\t}()\n\n\t\t\tclient, err := newWebSocketClient(agent)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toptions := client.getOptions()\n\n\t\t\t// Parse the WebSocket URL\n\t\t\twsURL, err := url.Parse(options.Addr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedScheme, wsURL.Scheme)\n\t\t\tassert.Equal(t, tc.expectedPath, wsURL.Path)\n\n\t\t\t// Check headers\n\t\t\tassert.Equal(t, \"test-token\", options.RequestHeader.Get(\"X-Token\"))\n\t\t\tassert.Equal(t, beszel.Version, options.RequestHeader.Get(\"X-Beszel\"))\n\t\t\tassert.Contains(t, options.RequestHeader.Get(\"User-Agent\"), \"Mozilla/5.0\")\n\n\t\t\t// Test options caching\n\t\t\toptions2 := client.getOptions()\n\t\t\tassert.Same(t, options, options2, \"Options should be cached\")\n\t\t})\n\t}\n}\n\n// TestWebSocketClient_VerifySignature tests signature verification\nfunc TestWebSocketClient_VerifySignature(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\t// Generate test key pairs\n\t_, goodPrivKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tgoodPubKey, err := ssh.NewPublicKey(goodPrivKey.Public().(ed25519.PublicKey))\n\trequire.NoError(t, err)\n\n\t_, badPrivKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tbadPubKey, err := ssh.NewPublicKey(badPrivKey.Public().(ed25519.PublicKey))\n\trequire.NoError(t, err)\n\n\t// Set up environment\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"http://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\tclient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tkeys        []ssh.PublicKey\n\t\ttoken       string\n\t\tsignWith    ed25519.PrivateKey\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"valid signature with correct key\",\n\t\t\tkeys:        []ssh.PublicKey{goodPubKey},\n\t\t\ttoken:       \"test-token\",\n\t\t\tsignWith:    goodPrivKey,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid signature with wrong key\",\n\t\t\tkeys:        []ssh.PublicKey{goodPubKey},\n\t\t\ttoken:       \"test-token\",\n\t\t\tsignWith:    badPrivKey,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid signature with multiple keys\",\n\t\t\tkeys:        []ssh.PublicKey{badPubKey, goodPubKey},\n\t\t\ttoken:       \"test-token\",\n\t\t\tsignWith:    goodPrivKey,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"no valid keys\",\n\t\t\tkeys:        []ssh.PublicKey{badPubKey},\n\t\t\ttoken:       \"test-token\",\n\t\t\tsignWith:    goodPrivKey,\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Set up agent with test keys\n\t\t\tagent.keys = tc.keys\n\t\t\tclient.token = tc.token\n\n\t\t\t// Create signature\n\t\t\tsignature := ed25519.Sign(tc.signWith, []byte(tc.token))\n\n\t\t\terr := client.verifySignature(signature)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), \"invalid signature\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWebSocketClient_HandleHubRequest tests hub request routing (basic verification logic)\nfunc TestWebSocketClient_HandleHubRequest(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\t// Set up environment\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"http://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\tclient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\taction      common.WebSocketAction\n\t\thubVerified bool\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname:        \"CheckFingerprint without verification\",\n\t\t\taction:      common.CheckFingerprint,\n\t\t\thubVerified: false,\n\t\t\texpectError: false, // CheckFingerprint is allowed without verification\n\t\t},\n\t\t{\n\t\t\tname:        \"GetData without verification\",\n\t\t\taction:      common.GetData,\n\t\t\thubVerified: false,\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"hub not verified\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tclient.hubVerified = tc.hubVerified\n\n\t\t\t// Create minimal request\n\t\t\thubRequest := &common.HubRequest[cbor.RawMessage]{\n\t\t\t\tAction: tc.action,\n\t\t\t\tData:   cbor.RawMessage{},\n\t\t\t}\n\n\t\t\terr := client.handleHubRequest(hubRequest, nil)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tif tc.errorMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tc.errorMsg)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// For CheckFingerprint, we expect a decode error since we're not providing valid data,\n\t\t\t\t// but it shouldn't be the \"hub not verified\" error\n\t\t\t\tif err != nil && tc.errorMsg != \"\" {\n\t\t\t\t\tassert.NotContains(t, err.Error(), tc.errorMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWebSocketClient_GetUserAgent tests user agent generation\nfunc TestGetUserAgent(t *testing.T) {\n\t// Run multiple times to check both variants\n\tuserAgents := make(map[string]bool)\n\n\tfor range 20 {\n\t\tua := getUserAgent()\n\t\tuserAgents[ua] = true\n\n\t\t// Check that it's a valid Mozilla user agent\n\t\tassert.Contains(t, ua, \"Mozilla/5.0\")\n\t\tassert.Contains(t, ua, \"AppleWebKit/537.36\")\n\t\tassert.Contains(t, ua, \"Chrome/124.0.0.0\")\n\t\tassert.Contains(t, ua, \"Safari/537.36\")\n\n\t\t// Should contain either Windows or Mac\n\t\tisWindows := strings.Contains(ua, \"Windows NT 11.0\")\n\t\tisMac := strings.Contains(ua, \"Macintosh; Intel Mac OS X 14_0_0\")\n\t\tassert.True(t, isWindows || isMac, \"User agent should contain either Windows or Mac identifier\")\n\t}\n\n\t// With enough iterations, we should see both variants\n\t// though this might occasionally fail\n\tif len(userAgents) == 1 {\n\t\tt.Log(\"Note: Only one user agent variant was generated in this test run\")\n\t}\n}\n\n// TestWebSocketClient_Close tests connection closing\nfunc TestWebSocketClient_Close(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\t// Set up environment\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"http://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\tclient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\n\t// Test closing with nil connection (should not panic)\n\tassert.NotPanics(t, func() {\n\t\tclient.Close()\n\t})\n}\n\n// TestWebSocketClient_ConnectRateLimit tests connection rate limiting\nfunc TestWebSocketClient_ConnectRateLimit(t *testing.T) {\n\tagent := createTestAgent(t)\n\n\t// Set up environment\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"http://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\tclient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\n\t// Set recent connection attempt\n\tclient.lastConnectAttempt = time.Now()\n\n\t// Test that connection fails quickly due to rate limiting\n\t// This won't actually connect but should fail fast\n\terr = client.Connect()\n\tassert.Error(t, err, \"Connection should fail but not hang\")\n}\n\n// TestGetToken tests the getToken function with various scenarios\nfunc TestGetToken(t *testing.T) {\n\tunsetEnvVars := func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\tos.Unsetenv(\"TOKEN\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN_FILE\")\n\t\tos.Unsetenv(\"TOKEN_FILE\")\n\t}\n\n\tt.Run(\"token from TOKEN environment variable\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Set TOKEN env var\n\t\texpectedToken := \"test-token-from-env\"\n\t\tos.Setenv(\"TOKEN\", expectedToken)\n\t\tdefer os.Unsetenv(\"TOKEN\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedToken, token)\n\t})\n\n\tt.Run(\"token from BESZEL_AGENT_TOKEN environment variable\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Set BESZEL_AGENT_TOKEN env var (should take precedence)\n\t\texpectedToken := \"test-token-from-beszel-env\"\n\t\tos.Setenv(\"BESZEL_AGENT_TOKEN\", expectedToken)\n\t\tdefer os.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedToken, token)\n\t})\n\n\tt.Run(\"token from TOKEN_FILE\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Create a temporary token file\n\t\texpectedToken := \"test-token-from-file\"\n\t\ttokenFile, err := os.CreateTemp(\"\", \"token-test-*.txt\")\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tokenFile.Name())\n\n\t\t_, err = tokenFile.WriteString(expectedToken)\n\t\trequire.NoError(t, err)\n\t\ttokenFile.Close()\n\n\t\t// Set TOKEN_FILE env var\n\t\tos.Setenv(\"TOKEN_FILE\", tokenFile.Name())\n\t\tdefer os.Unsetenv(\"TOKEN_FILE\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedToken, token)\n\t})\n\n\tt.Run(\"token from BESZEL_AGENT_TOKEN_FILE\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Create a temporary token file\n\t\texpectedToken := \"test-token-from-beszel-file\"\n\t\ttokenFile, err := os.CreateTemp(\"\", \"token-test-*.txt\")\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tokenFile.Name())\n\n\t\t_, err = tokenFile.WriteString(expectedToken)\n\t\trequire.NoError(t, err)\n\t\ttokenFile.Close()\n\n\t\t// Set BESZEL_AGENT_TOKEN_FILE env var (should take precedence)\n\t\tos.Setenv(\"BESZEL_AGENT_TOKEN_FILE\", tokenFile.Name())\n\t\tdefer os.Unsetenv(\"BESZEL_AGENT_TOKEN_FILE\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedToken, token)\n\t})\n\n\tt.Run(\"TOKEN takes precedence over TOKEN_FILE\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Create a temporary token file\n\t\tfileToken := \"token-from-file\"\n\t\ttokenFile, err := os.CreateTemp(\"\", \"token-test-*.txt\")\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tokenFile.Name())\n\n\t\t_, err = tokenFile.WriteString(fileToken)\n\t\trequire.NoError(t, err)\n\t\ttokenFile.Close()\n\n\t\t// Set both TOKEN and TOKEN_FILE\n\t\tenvToken := \"token-from-env\"\n\t\tos.Setenv(\"TOKEN\", envToken)\n\t\tos.Setenv(\"TOKEN_FILE\", tokenFile.Name())\n\t\tdefer func() {\n\t\t\tos.Unsetenv(\"TOKEN\")\n\t\t\tos.Unsetenv(\"TOKEN_FILE\")\n\t\t}()\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, envToken, token, \"TOKEN should take precedence over TOKEN_FILE\")\n\t})\n\n\tt.Run(\"error when neither TOKEN nor TOKEN_FILE is set\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\ttoken, err := getToken()\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, \"\", token)\n\t\tassert.Contains(t, err.Error(), \"must set TOKEN or TOKEN_FILE\")\n\t})\n\n\tt.Run(\"error when TOKEN_FILE points to non-existent file\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Set TOKEN_FILE to a non-existent file\n\t\tos.Setenv(\"TOKEN_FILE\", \"/non/existent/file.txt\")\n\t\tdefer os.Unsetenv(\"TOKEN_FILE\")\n\n\t\ttoken, err := getToken()\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, \"\", token)\n\t\tassert.Contains(t, err.Error(), \"no such file or directory\")\n\t})\n\n\tt.Run(\"handles empty token file\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\t// Create an empty token file\n\t\ttokenFile, err := os.CreateTemp(\"\", \"token-test-*.txt\")\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tokenFile.Name())\n\t\ttokenFile.Close()\n\n\t\t// Set TOKEN_FILE env var\n\t\tos.Setenv(\"TOKEN_FILE\", tokenFile.Name())\n\t\tdefer os.Unsetenv(\"TOKEN_FILE\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"\", token, \"Empty file should return empty string\")\n\t})\n\n\tt.Run(\"strips whitespace from TOKEN_FILE\", func(t *testing.T) {\n\t\tunsetEnvVars()\n\n\t\ttokenWithWhitespace := \"  test-token-with-whitespace  \\n\\t\"\n\t\texpectedToken := \"test-token-with-whitespace\"\n\t\ttokenFile, err := os.CreateTemp(\"\", \"token-test-*.txt\")\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tokenFile.Name())\n\n\t\t_, err = tokenFile.WriteString(tokenWithWhitespace)\n\t\trequire.NoError(t, err)\n\t\ttokenFile.Close()\n\n\t\tos.Setenv(\"TOKEN_FILE\", tokenFile.Name())\n\t\tdefer os.Unsetenv(\"TOKEN_FILE\")\n\n\t\ttoken, err := getToken()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedToken, token, \"Whitespace should be stripped from token file content\")\n\t})\n}\n"
  },
  {
    "path": "agent/connection_manager.go",
    "content": "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/agent/health\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\n// ConnectionManager manages the connection state and events for the agent.\n// It handles both WebSocket and SSH connections, automatically switching between\n// them based on availability and managing reconnection attempts.\ntype ConnectionManager struct {\n\tagent          *Agent               // Reference to the parent agent\n\tState          ConnectionState      // Current connection state\n\teventChan      chan ConnectionEvent // Channel for connection events\n\twsClient       *WebSocketClient     // WebSocket client for hub communication\n\tserverOptions  ServerOptions        // Configuration for SSH server\n\twsTicker       *time.Ticker         // Ticker for WebSocket connection attempts\n\tisConnecting   bool                 // Prevents multiple simultaneous reconnection attempts\n\tConnectionType system.ConnectionType\n}\n\n// ConnectionState represents the current connection state of the agent.\ntype ConnectionState uint8\n\n// ConnectionEvent represents connection-related events that can occur.\ntype ConnectionEvent uint8\n\n// Connection states\nconst (\n\tDisconnected       ConnectionState = iota // No active connection\n\tWebSocketConnected                        // Connected via WebSocket\n\tSSHConnected                              // Connected via SSH\n)\n\n// Connection events\nconst (\n\tWebSocketConnect    ConnectionEvent = iota // WebSocket connection established\n\tWebSocketDisconnect                        // WebSocket connection lost\n\tSSHConnect                                 // SSH connection established\n\tSSHDisconnect                              // SSH connection lost\n)\n\nconst wsTickerInterval = 10 * time.Second\n\n// newConnectionManager creates a new connection manager for the given agent.\nfunc newConnectionManager(agent *Agent) *ConnectionManager {\n\tcm := &ConnectionManager{\n\t\tagent: agent,\n\t\tState: Disconnected,\n\t}\n\treturn cm\n}\n\n// startWsTicker starts or resets the WebSocket connection attempt ticker.\nfunc (c *ConnectionManager) startWsTicker() {\n\tif c.wsTicker == nil {\n\t\tc.wsTicker = time.NewTicker(wsTickerInterval)\n\t} else {\n\t\tc.wsTicker.Reset(wsTickerInterval)\n\t}\n}\n\n// stopWsTicker stops the WebSocket connection attempt ticker.\nfunc (c *ConnectionManager) stopWsTicker() {\n\tif c.wsTicker != nil {\n\t\tc.wsTicker.Stop()\n\t}\n}\n\n// Start begins connection attempts and enters the main event loop.\n// It handles connection events, periodic health updates, and graceful shutdown.\nfunc (c *ConnectionManager) Start(serverOptions ServerOptions) error {\n\tif c.eventChan != nil {\n\t\treturn errors.New(\"already started\")\n\t}\n\n\twsClient, err := newWebSocketClient(c.agent)\n\tif err != nil {\n\t\tslog.Warn(\"Error creating WebSocket client\", \"err\", err)\n\t}\n\tc.wsClient = wsClient\n\n\tc.serverOptions = serverOptions\n\tc.eventChan = make(chan ConnectionEvent, 1)\n\n\t// signal handling for shutdown\n\tsigCtx, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n\tdefer stopSignals()\n\n\tc.startWsTicker()\n\tc.connect()\n\n\t// update health status immediately and every 90 seconds\n\t_ = health.Update()\n\thealthTicker := time.Tick(90 * time.Second)\n\n\tfor {\n\t\tselect {\n\t\tcase connectionEvent := <-c.eventChan:\n\t\t\tc.handleEvent(connectionEvent)\n\t\tcase <-c.wsTicker.C:\n\t\t\t_ = c.startWebSocketConnection()\n\t\tcase <-healthTicker:\n\t\t\t_ = health.Update()\n\t\tcase <-sigCtx.Done():\n\t\t\tslog.Info(\"Shutting down\", \"cause\", context.Cause(sigCtx))\n\t\t\t_ = c.agent.StopServer()\n\t\t\tc.closeWebSocket()\n\t\t\treturn health.CleanUp()\n\t\t}\n\t}\n}\n\n// handleEvent processes connection events and updates the connection state accordingly.\nfunc (c *ConnectionManager) handleEvent(event ConnectionEvent) {\n\tswitch event {\n\tcase WebSocketConnect:\n\t\tc.handleStateChange(WebSocketConnected)\n\tcase SSHConnect:\n\t\tc.handleStateChange(SSHConnected)\n\tcase WebSocketDisconnect:\n\t\tif c.State == WebSocketConnected {\n\t\t\tc.handleStateChange(Disconnected)\n\t\t}\n\tcase SSHDisconnect:\n\t\tif c.State == SSHConnected {\n\t\t\tc.handleStateChange(Disconnected)\n\t\t}\n\t}\n}\n\n// handleStateChange updates the connection state and performs necessary actions\n// based on the new state, including stopping services and initiating reconnections.\nfunc (c *ConnectionManager) handleStateChange(newState ConnectionState) {\n\tif c.State == newState {\n\t\treturn\n\t}\n\tc.State = newState\n\tswitch newState {\n\tcase WebSocketConnected:\n\t\tslog.Info(\"WebSocket connected\", \"host\", c.wsClient.hubURL.Host)\n\t\tc.ConnectionType = system.ConnectionTypeWebSocket\n\t\tc.stopWsTicker()\n\t\t_ = c.agent.StopServer()\n\t\tc.isConnecting = false\n\tcase SSHConnected:\n\t\t// stop new ws connection attempts\n\t\tslog.Info(\"SSH connection established\")\n\t\tc.ConnectionType = system.ConnectionTypeSSH\n\t\tc.stopWsTicker()\n\t\tc.isConnecting = false\n\tcase Disconnected:\n\t\tc.ConnectionType = system.ConnectionTypeNone\n\t\tif c.isConnecting {\n\t\t\t// Already handling reconnection, avoid duplicate attempts\n\t\t\treturn\n\t\t}\n\t\tc.isConnecting = true\n\t\tslog.Warn(\"Disconnected from hub\")\n\t\t// make sure old ws connection is closed\n\t\tc.closeWebSocket()\n\t\t// reconnect\n\t\tgo c.connect()\n\t}\n}\n\n// connect handles the connection logic with proper delays and priority.\n// It attempts WebSocket connection first, falling back to SSH server if needed.\nfunc (c *ConnectionManager) connect() {\n\tc.isConnecting = true\n\tdefer func() {\n\t\tc.isConnecting = false\n\t}()\n\n\tif c.wsClient != nil && time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {\n\t\ttime.Sleep(5 * time.Second)\n\t}\n\n\t// Try WebSocket first, if it fails, start SSH server\n\terr := c.startWebSocketConnection()\n\tif err != nil && c.State == Disconnected {\n\t\tc.startSSHServer()\n\t\tc.startWsTicker()\n\t}\n}\n\n// startWebSocketConnection attempts to establish a WebSocket connection to the hub.\nfunc (c *ConnectionManager) startWebSocketConnection() error {\n\tif c.State != Disconnected {\n\t\treturn errors.New(\"already connected\")\n\t}\n\tif c.wsClient == nil {\n\t\treturn errors.New(\"WebSocket client not initialized\")\n\t}\n\tif time.Since(c.wsClient.lastConnectAttempt) < 5*time.Second {\n\t\treturn errors.New(\"already connecting\")\n\t}\n\n\terr := c.wsClient.Connect()\n\tif err != nil {\n\t\tslog.Warn(\"WebSocket connection failed\", \"err\", err)\n\t\tc.closeWebSocket()\n\t}\n\treturn err\n}\n\n// startSSHServer starts the SSH server if the agent is currently disconnected.\nfunc (c *ConnectionManager) startSSHServer() {\n\tif c.State == Disconnected {\n\t\tgo c.agent.StartServer(c.serverOptions)\n\t}\n}\n\n// closeWebSocket closes the WebSocket connection if it exists.\nfunc (c *ConnectionManager) closeWebSocket() {\n\tif c.wsClient != nil {\n\t\tc.wsClient.Close()\n\t}\n}\n"
  },
  {
    "path": "agent/connection_manager_test.go",
    "content": "//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\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc createTestAgent(t *testing.T) *Agent {\n\tdataDir := t.TempDir()\n\tagent, err := NewAgent(dataDir)\n\trequire.NoError(t, err)\n\treturn agent\n}\n\nfunc createTestServerOptions(t *testing.T) ServerOptions {\n\t// Generate test key pair\n\t_, privKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tsshPubKey, err := ssh.NewPublicKey(privKey.Public().(ed25519.PublicKey))\n\trequire.NoError(t, err)\n\n\t// Find available port\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tport := listener.Addr().(*net.TCPAddr).Port\n\tlistener.Close()\n\n\treturn ServerOptions{\n\t\tNetwork: \"tcp\",\n\t\tAddr:    fmt.Sprintf(\"127.0.0.1:%d\", port),\n\t\tKeys:    []ssh.PublicKey{sshPubKey},\n\t}\n}\n\n// TestConnectionManager_NewConnectionManager tests connection manager creation\nfunc TestConnectionManager_NewConnectionManager(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := newConnectionManager(agent)\n\n\tassert.NotNil(t, cm, \"Connection manager should not be nil\")\n\tassert.Equal(t, agent, cm.agent, \"Agent reference should be set\")\n\tassert.Equal(t, Disconnected, cm.State, \"Initial state should be Disconnected\")\n\tassert.Nil(t, cm.eventChan, \"Event channel should be nil initially\")\n\tassert.Nil(t, cm.wsClient, \"WebSocket client should be nil initially\")\n\tassert.Nil(t, cm.wsTicker, \"WebSocket ticker should be nil initially\")\n\tassert.False(t, cm.isConnecting, \"isConnecting should be false initially\")\n}\n\n// TestConnectionManager_StateTransitions tests basic state transitions\nfunc TestConnectionManager_StateTransitions(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\tinitialState := cm.State\n\tcm.wsClient = &WebSocketClient{\n\t\thubURL: &url.URL{\n\t\t\tHost: \"localhost:8080\",\n\t\t},\n\t}\n\tassert.NotNil(t, cm, \"Connection manager should not be nil\")\n\tassert.Equal(t, Disconnected, initialState, \"Initial state should be Disconnected\")\n\n\t// Test state transitions\n\tcm.handleStateChange(WebSocketConnected)\n\tassert.Equal(t, WebSocketConnected, cm.State, \"State should change to WebSocketConnected\")\n\n\tcm.handleStateChange(SSHConnected)\n\tassert.Equal(t, SSHConnected, cm.State, \"State should change to SSHConnected\")\n\n\tcm.handleStateChange(Disconnected)\n\tassert.Equal(t, Disconnected, cm.State, \"State should change to Disconnected\")\n\n\t// Test that same state doesn't trigger changes\n\tcm.State = WebSocketConnected\n\tcm.handleStateChange(WebSocketConnected)\n\tassert.Equal(t, WebSocketConnected, cm.State, \"Same state should not trigger change\")\n}\n\n// TestConnectionManager_EventHandling tests event handling logic\nfunc TestConnectionManager_EventHandling(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\tcm.wsClient = &WebSocketClient{\n\t\thubURL: &url.URL{\n\t\t\tHost: \"localhost:8080\",\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tinitialState  ConnectionState\n\t\tevent         ConnectionEvent\n\t\texpectedState ConnectionState\n\t}{\n\t\t{\n\t\t\tname:          \"WebSocket connect from disconnected\",\n\t\t\tinitialState:  Disconnected,\n\t\t\tevent:         WebSocketConnect,\n\t\t\texpectedState: WebSocketConnected,\n\t\t},\n\t\t{\n\t\t\tname:          \"SSH connect from disconnected\",\n\t\t\tinitialState:  Disconnected,\n\t\t\tevent:         SSHConnect,\n\t\t\texpectedState: SSHConnected,\n\t\t},\n\t\t{\n\t\t\tname:          \"WebSocket disconnect from connected\",\n\t\t\tinitialState:  WebSocketConnected,\n\t\t\tevent:         WebSocketDisconnect,\n\t\t\texpectedState: Disconnected,\n\t\t},\n\t\t{\n\t\t\tname:          \"SSH disconnect from connected\",\n\t\t\tinitialState:  SSHConnected,\n\t\t\tevent:         SSHDisconnect,\n\t\t\texpectedState: Disconnected,\n\t\t},\n\t\t{\n\t\t\tname:          \"WebSocket disconnect from SSH connected (no change)\",\n\t\t\tinitialState:  SSHConnected,\n\t\t\tevent:         WebSocketDisconnect,\n\t\t\texpectedState: SSHConnected,\n\t\t},\n\t\t{\n\t\t\tname:          \"SSH disconnect from WebSocket connected (no change)\",\n\t\t\tinitialState:  WebSocketConnected,\n\t\t\tevent:         SSHDisconnect,\n\t\t\texpectedState: WebSocketConnected,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcm.State = tc.initialState\n\t\t\tcm.handleEvent(tc.event)\n\t\t\tassert.Equal(t, tc.expectedState, cm.State, \"State should match expected after event\")\n\t\t})\n\t}\n}\n\n// TestConnectionManager_TickerManagement tests WebSocket ticker management\nfunc TestConnectionManager_TickerManagement(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\n\t// Test starting ticker\n\tcm.startWsTicker()\n\tassert.NotNil(t, cm.wsTicker, \"Ticker should be created\")\n\n\t// Test stopping ticker (should not panic)\n\tassert.NotPanics(t, func() {\n\t\tcm.stopWsTicker()\n\t}, \"Stopping ticker should not panic\")\n\n\t// Test stopping nil ticker (should not panic)\n\tcm.wsTicker = nil\n\tassert.NotPanics(t, func() {\n\t\tcm.stopWsTicker()\n\t}, \"Stopping nil ticker should not panic\")\n\n\t// Test restarting ticker\n\tcm.startWsTicker()\n\tassert.NotNil(t, cm.wsTicker, \"Ticker should be recreated\")\n\n\t// Test resetting existing ticker\n\tfirstTicker := cm.wsTicker\n\tcm.startWsTicker()\n\tassert.Equal(t, firstTicker, cm.wsTicker, \"Same ticker instance should be reused\")\n\n\tcm.stopWsTicker()\n}\n\n// TestConnectionManager_WebSocketConnectionFlow tests WebSocket connection logic\nfunc TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping WebSocket connection test in short mode\")\n\t}\n\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\n\t// Test WebSocket connection without proper environment\n\terr := cm.startWebSocketConnection()\n\tassert.Error(t, err, \"WebSocket connection should fail without proper environment\")\n\tassert.Equal(t, Disconnected, cm.State, \"State should remain Disconnected after failed connection\")\n\n\t// Test with invalid URL\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"invalid-url\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\t// Test with missing token\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"http://localhost:8080\")\n\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\n\t_, err2 := newWebSocketClient(agent)\n\tassert.Error(t, err2, \"WebSocket client creation should fail without token\")\n}\n\n// TestConnectionManager_ReconnectionLogic tests reconnection prevention logic\nfunc TestConnectionManager_ReconnectionLogic(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\tcm.eventChan = make(chan ConnectionEvent, 1)\n\n\t// Test that isConnecting flag prevents duplicate reconnection attempts\n\t// Start from connected state, then simulate disconnect\n\tcm.State = WebSocketConnected\n\tcm.isConnecting = false\n\n\t// First disconnect should trigger reconnection logic\n\tcm.handleStateChange(Disconnected)\n\tassert.Equal(t, Disconnected, cm.State, \"Should change to disconnected\")\n\tassert.True(t, cm.isConnecting, \"Should set isConnecting flag\")\n}\n\n// TestConnectionManager_ConnectWithRateLimit tests connection rate limiting\nfunc TestConnectionManager_ConnectWithRateLimit(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\n\t// Set up environment for WebSocket client creation\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"ws://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\t// Create WebSocket client\n\twsClient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\tcm.wsClient = wsClient\n\n\t// Set recent connection attempt\n\tcm.wsClient.lastConnectAttempt = time.Now()\n\n\t// Test that connection is rate limited\n\terr = cm.startWebSocketConnection()\n\tassert.Error(t, err, \"Should error due to rate limiting\")\n\tassert.Contains(t, err.Error(), \"already connecting\", \"Error should indicate rate limiting\")\n\n\t// Test connection after rate limit expires\n\tcm.wsClient.lastConnectAttempt = time.Now().Add(-10 * time.Second)\n\terr = cm.startWebSocketConnection()\n\t// This will fail due to no actual server, but should not be rate limited\n\tassert.Error(t, err, \"Connection should fail but not due to rate limiting\")\n\tassert.NotContains(t, err.Error(), \"already connecting\", \"Error should not indicate rate limiting\")\n}\n\n// TestConnectionManager_StartWithInvalidConfig tests starting with invalid configuration\nfunc TestConnectionManager_StartWithInvalidConfig(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\tserverOptions := createTestServerOptions(t)\n\n\t// Test starting when already started\n\tcm.eventChan = make(chan ConnectionEvent, 5)\n\terr := cm.Start(serverOptions)\n\tassert.Error(t, err, \"Should error when starting already started connection manager\")\n}\n\n// TestConnectionManager_CloseWebSocket tests WebSocket closing\nfunc TestConnectionManager_CloseWebSocket(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\n\t// Test closing when no WebSocket client exists\n\tassert.NotPanics(t, func() {\n\t\tcm.closeWebSocket()\n\t}, \"Should not panic when closing nil WebSocket client\")\n\n\t// Set up environment and create WebSocket client\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", \"ws://localhost:8080\")\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", \"test-token\")\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\twsClient, err := newWebSocketClient(agent)\n\trequire.NoError(t, err)\n\tcm.wsClient = wsClient\n\n\t// Test closing when WebSocket client exists\n\tassert.NotPanics(t, func() {\n\t\tcm.closeWebSocket()\n\t}, \"Should not panic when closing WebSocket client\")\n}\n\n// TestConnectionManager_ConnectFlow tests the connect method\nfunc TestConnectionManager_ConnectFlow(t *testing.T) {\n\tagent := createTestAgent(t)\n\tcm := agent.connectionManager\n\n\t// Test connect without WebSocket client\n\tassert.NotPanics(t, func() {\n\t\tcm.connect()\n\t}, \"Connect should not panic without WebSocket client\")\n}\n"
  },
  {
    "path": "agent/cpu.go",
    "content": "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/gopsutil/v4/cpu\"\n)\n\nvar lastCpuTimes = make(map[uint16]cpu.TimesStat)\nvar lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)\n\n// init initializes the CPU monitoring by storing the initial CPU times\n// for the default 60-second cache interval.\nfunc init() {\n\tif times, err := cpu.Times(false); err == nil && len(times) > 0 {\n\t\tlastCpuTimes[60000] = times[0]\n\t}\n\tif perCoreTimes, err := cpu.Times(true); err == nil && len(perCoreTimes) > 0 {\n\t\tlastPerCoreCpuTimes[60000] = perCoreTimes\n\t}\n}\n\n// CpuMetrics contains detailed CPU usage breakdown\ntype CpuMetrics struct {\n\tTotal  float64\n\tUser   float64\n\tSystem float64\n\tIowait float64\n\tSteal  float64\n\tIdle   float64\n}\n\n// getCpuMetrics calculates detailed CPU usage metrics using cached previous measurements.\n// It returns percentages for total, user, system, iowait, and steal time.\nfunc getCpuMetrics(cacheTimeMs uint16) (CpuMetrics, error) {\n\ttimes, err := cpu.Times(false)\n\tif err != nil || len(times) == 0 {\n\t\treturn CpuMetrics{}, err\n\t}\n\t// if cacheTimeMs is not in lastCpuTimes, use 60000 as fallback lastCpuTime\n\tif _, ok := lastCpuTimes[cacheTimeMs]; !ok {\n\t\tlastCpuTimes[cacheTimeMs] = lastCpuTimes[60000]\n\t}\n\n\tt1 := lastCpuTimes[cacheTimeMs]\n\tt2 := times[0]\n\n\tt1All, _ := getAllBusy(t1)\n\tt2All, _ := getAllBusy(t2)\n\n\ttotalDelta := t2All - t1All\n\tif totalDelta <= 0 {\n\t\treturn CpuMetrics{}, nil\n\t}\n\n\tmetrics := CpuMetrics{\n\t\tTotal:  calculateBusy(t1, t2),\n\t\tUser:   clampPercent((t2.User - t1.User) / totalDelta * 100),\n\t\tSystem: clampPercent((t2.System - t1.System) / totalDelta * 100),\n\t\tIowait: clampPercent((t2.Iowait - t1.Iowait) / totalDelta * 100),\n\t\tSteal:  clampPercent((t2.Steal - t1.Steal) / totalDelta * 100),\n\t\tIdle:   clampPercent((t2.Idle - t1.Idle) / totalDelta * 100),\n\t}\n\n\tlastCpuTimes[cacheTimeMs] = times[0]\n\treturn metrics, nil\n}\n\n// clampPercent ensures the percentage is between 0 and 100\nfunc clampPercent(value float64) float64 {\n\treturn math.Min(100, math.Max(0, value))\n}\n\n// getPerCoreCpuUsage calculates per-core CPU busy usage as integer percentages (0-100).\n// It uses cached previous measurements for the provided cache interval.\nfunc getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {\n\tperCoreTimes, err := cpu.Times(true)\n\tif err != nil || len(perCoreTimes) == 0 {\n\t\treturn nil, err\n\t}\n\n\t// Initialize cache if needed\n\tif _, ok := lastPerCoreCpuTimes[cacheTimeMs]; !ok {\n\t\tlastPerCoreCpuTimes[cacheTimeMs] = lastPerCoreCpuTimes[60000]\n\t}\n\n\tlastTimes := lastPerCoreCpuTimes[cacheTimeMs]\n\n\t// Limit to the number of cores available in both samples\n\tlength := min(len(lastTimes), len(perCoreTimes))\n\n\tusage := make([]uint8, length)\n\tfor i := 0; i < length; i++ {\n\t\tt1 := lastTimes[i]\n\t\tt2 := perCoreTimes[i]\n\t\tusage[i] = uint8(math.Round(calculateBusy(t1, t2)))\n\t}\n\n\tlastPerCoreCpuTimes[cacheTimeMs] = perCoreTimes\n\treturn usage, nil\n}\n\n// calculateBusy calculates the CPU busy percentage between two time points.\n// It computes the ratio of busy time to total time elapsed between t1 and t2,\n// returning a percentage clamped between 0 and 100.\nfunc calculateBusy(t1, t2 cpu.TimesStat) float64 {\n\tt1All, t1Busy := getAllBusy(t1)\n\tt2All, t2Busy := getAllBusy(t2)\n\n\tif t2All <= t1All || t2Busy <= t1Busy {\n\t\treturn 0\n\t}\n\treturn clampPercent((t2Busy - t1Busy) / (t2All - t1All) * 100)\n}\n\n// getAllBusy calculates the total CPU time and busy CPU time from CPU times statistics.\n// On Linux, it excludes guest and guest_nice time from the total to match kernel behavior.\n// Returns total CPU time and busy CPU time (total minus idle and I/O wait time).\nfunc getAllBusy(t cpu.TimesStat) (float64, float64) {\n\ttot := t.Total()\n\tif runtime.GOOS == \"linux\" {\n\t\ttot -= t.Guest     // Linux 2.6.24+\n\t\ttot -= t.GuestNice // Linux 3.2.0+\n\t}\n\n\tbusy := tot - t.Idle - t.Iowait\n\n\treturn tot, busy\n}\n"
  },
  {
    "path": "agent/data_dir.go",
    "content": "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// GetDataDir returns the path to the data directory for the agent and an error\n// if the directory is not valid. Attempts to find the optimal data directory if\n// no data directories are provided.\nfunc GetDataDir(dataDirs ...string) (string, error) {\n\tif len(dataDirs) > 0 {\n\t\treturn testDataDirs(dataDirs)\n\t}\n\n\tdataDir, _ := utils.GetEnv(\"DATA_DIR\")\n\tif dataDir != \"\" {\n\t\tdataDirs = append(dataDirs, dataDir)\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tdataDirs = append(dataDirs,\n\t\t\tfilepath.Join(os.Getenv(\"APPDATA\"), \"beszel-agent\"),\n\t\t\tfilepath.Join(os.Getenv(\"LOCALAPPDATA\"), \"beszel-agent\"),\n\t\t)\n\t} else {\n\t\tdataDirs = append(dataDirs, \"/var/lib/beszel-agent\")\n\t\tif homeDir, err := os.UserHomeDir(); err == nil {\n\t\t\tdataDirs = append(dataDirs, filepath.Join(homeDir, \".config\", \"beszel\"))\n\t\t}\n\t}\n\treturn testDataDirs(dataDirs)\n}\n\nfunc testDataDirs(paths []string) (string, error) {\n\t// first check if the directory exists and is writable\n\tfor _, path := range paths {\n\t\tif valid, _ := isValidDataDir(path, false); valid {\n\t\t\treturn path, nil\n\t\t}\n\t}\n\t// if the directory doesn't exist, try to create it\n\tfor _, path := range paths {\n\t\texists, _ := directoryExists(path)\n\t\tif exists {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(path, 0755); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Verify the created directory is actually writable\n\t\twritable, _ := directoryIsWritable(path)\n\t\tif !writable {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn path, nil\n\t}\n\n\treturn \"\", errors.New(\"data directory not found\")\n}\n\nfunc isValidDataDir(path string, createIfNotExists bool) (bool, error) {\n\texists, err := directoryExists(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif !exists {\n\t\tif !createIfNotExists {\n\t\t\treturn false, nil\n\t\t}\n\t\tif err = os.MkdirAll(path, 0755); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\t// Always check if the directory is writable\n\twritable, err := directoryIsWritable(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn writable, nil\n}\n\n// directoryExists checks if a directory exists\nfunc directoryExists(path string) (bool, error) {\n\t// Check if directory exists\n\tstat, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\tif !stat.IsDir() {\n\t\treturn false, fmt.Errorf(\"%s is not a directory\", path)\n\t}\n\treturn true, nil\n}\n\n// directoryIsWritable tests if a directory is writable by creating and removing a temporary file\nfunc directoryIsWritable(path string) (bool, error) {\n\ttestFile := filepath.Join(path, \".write-test\")\n\tfile, err := os.Create(testFile)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer file.Close()\n\tdefer os.Remove(testFile)\n\treturn true, nil\n}\n"
  },
  {
    "path": "agent/data_dir_test.go",
    "content": "//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/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetDataDir(t *testing.T) {\n\t// Test with explicit dataDir parameter\n\tt.Run(\"explicit data dir\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tresult, err := GetDataDir(tempDir)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, tempDir, result)\n\t})\n\n\t// Test with explicit non-existent dataDir that can be created\n\tt.Run(\"explicit data dir - create new\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnewDir := filepath.Join(tempDir, \"new-data-dir\")\n\t\tresult, err := GetDataDir(newDir)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, newDir, result)\n\n\t\t// Verify directory was created\n\t\tstat, err := os.Stat(newDir)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, stat.IsDir())\n\t})\n\n\t// Test with DATA_DIR environment variable\n\tt.Run(\"DATA_DIR environment variable\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\n\t\t// Set environment variable\n\t\toldValue := os.Getenv(\"DATA_DIR\")\n\t\tdefer func() {\n\t\t\tif oldValue == \"\" {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_DATA_DIR\")\n\t\t\t} else {\n\t\t\t\tos.Setenv(\"BESZEL_AGENT_DATA_DIR\", oldValue)\n\t\t\t}\n\t\t}()\n\n\t\tos.Setenv(\"BESZEL_AGENT_DATA_DIR\", tempDir)\n\n\t\tresult, err := GetDataDir()\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, tempDir, result)\n\t})\n\n\t// Test with invalid explicit dataDir\n\tt.Run(\"invalid explicit data dir\", func(t *testing.T) {\n\t\tinvalidPath := \"/invalid/path/that/cannot/be/created\"\n\t\t_, err := GetDataDir(invalidPath)\n\t\tassert.Error(t, err)\n\t})\n\n\t// Test fallback behavior (empty dataDir, no env var)\n\tt.Run(\"fallback to default directories\", func(t *testing.T) {\n\t\t// Clear DATA_DIR environment variable\n\t\toldValue := os.Getenv(\"DATA_DIR\")\n\t\tdefer func() {\n\t\t\tif oldValue == \"\" {\n\t\t\t\tos.Unsetenv(\"DATA_DIR\")\n\t\t\t} else {\n\t\t\t\tos.Setenv(\"DATA_DIR\", oldValue)\n\t\t\t}\n\t\t}()\n\t\tos.Unsetenv(\"DATA_DIR\")\n\n\t\t// This will try platform-specific defaults, which may or may not work\n\t\t// We're mainly testing that it doesn't panic and returns some result\n\t\tresult, err := GetDataDir()\n\t\t// We don't assert success/failure here since it depends on system permissions\n\t\t// Just verify we get a string result if no error\n\t\tif err == nil {\n\t\t\tassert.NotEmpty(t, result)\n\t\t}\n\t})\n}\n\nfunc TestTestDataDirs(t *testing.T) {\n\t// Test with existing valid directory\n\tt.Run(\"existing valid directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tresult, err := testDataDirs([]string{tempDir})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, tempDir, result)\n\t})\n\n\t// Test with multiple directories, first one valid\n\tt.Run(\"multiple dirs - first valid\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tinvalidDir := \"/invalid/path\"\n\t\tresult, err := testDataDirs([]string{tempDir, invalidDir})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, tempDir, result)\n\t})\n\n\t// Test with multiple directories, second one valid\n\tt.Run(\"multiple dirs - second valid\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tinvalidDir := \"/invalid/path\"\n\t\tresult, err := testDataDirs([]string{invalidDir, tempDir})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, tempDir, result)\n\t})\n\n\t// Test with non-existing directory that can be created\n\tt.Run(\"create new directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnewDir := filepath.Join(tempDir, \"new-dir\")\n\t\tresult, err := testDataDirs([]string{newDir})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, newDir, result)\n\n\t\t// Verify directory was created\n\t\tstat, err := os.Stat(newDir)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, stat.IsDir())\n\t})\n\n\t// Test with no valid directories\n\tt.Run(\"no valid directories\", func(t *testing.T) {\n\t\tinvalidPaths := []string{\"/invalid/path1\", \"/invalid/path2\"}\n\t\t_, err := testDataDirs(invalidPaths)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"data directory not found\")\n\t})\n}\n\nfunc TestIsValidDataDir(t *testing.T) {\n\t// Test with existing directory\n\tt.Run(\"existing directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tvalid, err := isValidDataDir(tempDir, false)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, valid)\n\t})\n\n\t// Test with non-existing directory, createIfNotExists=false\n\tt.Run(\"non-existing dir - no create\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnonExistentDir := filepath.Join(tempDir, \"does-not-exist\")\n\t\tvalid, err := isValidDataDir(nonExistentDir, false)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, valid)\n\t})\n\n\t// Test with non-existing directory, createIfNotExists=true\n\tt.Run(\"non-existing dir - create\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnewDir := filepath.Join(tempDir, \"new-dir\")\n\t\tvalid, err := isValidDataDir(newDir, true)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, valid)\n\n\t\t// Verify directory was created\n\t\tstat, err := os.Stat(newDir)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, stat.IsDir())\n\t})\n\n\t// Test with file instead of directory\n\tt.Run(\"file instead of directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\ttempFile := filepath.Join(tempDir, \"testfile\")\n\t\terr := os.WriteFile(tempFile, []byte(\"test\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tvalid, err := isValidDataDir(tempFile, false)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, valid)\n\t\tassert.Contains(t, err.Error(), \"is not a directory\")\n\t})\n}\n\nfunc TestDirectoryExists(t *testing.T) {\n\t// Test with existing directory\n\tt.Run(\"existing directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\texists, err := directoryExists(tempDir)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, exists)\n\t})\n\n\t// Test with non-existing directory\n\tt.Run(\"non-existing directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnonExistentDir := filepath.Join(tempDir, \"does-not-exist\")\n\t\texists, err := directoryExists(nonExistentDir)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, exists)\n\t})\n\n\t// Test with file instead of directory\n\tt.Run(\"file instead of directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\ttempFile := filepath.Join(tempDir, \"testfile\")\n\t\terr := os.WriteFile(tempFile, []byte(\"test\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\texists, err := directoryExists(tempFile)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, exists)\n\t\tassert.Contains(t, err.Error(), \"is not a directory\")\n\t})\n}\n\nfunc TestDirectoryIsWritable(t *testing.T) {\n\t// Test with writable directory\n\tt.Run(\"writable directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\twritable, err := directoryIsWritable(tempDir)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, writable)\n\t})\n\n\t// Test with non-existing directory\n\tt.Run(\"non-existing directory\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\t\tnonExistentDir := filepath.Join(tempDir, \"does-not-exist\")\n\t\twritable, err := directoryIsWritable(nonExistentDir)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, writable)\n\t})\n\n\t// Test with non-writable directory (Unix-like systems only)\n\tt.Run(\"non-writable directory\", func(t *testing.T) {\n\t\tif runtime.GOOS != \"linux\" && runtime.GOOS != \"darwin\" {\n\t\t\tt.Skip(\"Skipping non-writable directory test on\", runtime.GOOS)\n\t\t}\n\n\t\ttempDir := t.TempDir()\n\t\treadOnlyDir := filepath.Join(tempDir, \"readonly\")\n\n\t\t// Create the directory\n\t\terr := os.Mkdir(readOnlyDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\t// Make it read-only\n\t\terr = os.Chmod(readOnlyDir, 0444)\n\t\trequire.NoError(t, err)\n\n\t\t// Restore permissions after test for cleanup\n\t\tdefer func() {\n\t\t\tos.Chmod(readOnlyDir, 0755)\n\t\t}()\n\n\t\twritable, err := directoryIsWritable(readOnlyDir)\n\t\tassert.Error(t, err)\n\t\tassert.False(t, writable)\n\t})\n}\n"
  },
  {
    "path": "agent/deltatracker/deltatracker.go",
    "content": "// Package deltatracker provides a tracker for calculating differences in numeric values over time.\npackage deltatracker\n\nimport (\n\t\"sync\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\n// Numeric is a constraint that permits any integer or floating-point type.\ntype Numeric interface {\n\tconstraints.Integer | constraints.Float\n}\n\n// DeltaTracker is a generic, thread-safe tracker for calculating differences\n// in numeric values over time.\n// K is the key type (e.g., int, string).\n// V is the value type (e.g., int, int64, float32, float64).\ntype DeltaTracker[K comparable, V Numeric] struct {\n\tsync.RWMutex\n\tcurrent  map[K]V\n\tprevious map[K]V\n}\n\n// NewDeltaTracker creates a new generic tracker.\nfunc NewDeltaTracker[K comparable, V Numeric]() *DeltaTracker[K, V] {\n\treturn &DeltaTracker[K, V]{\n\t\tcurrent:  make(map[K]V),\n\t\tprevious: make(map[K]V),\n\t}\n}\n\n// Set records the current value for a given ID.\nfunc (t *DeltaTracker[K, V]) Set(id K, value V) {\n\tt.Lock()\n\tdefer t.Unlock()\n\tt.current[id] = value\n}\n\n// Snapshot returns a copy of the current map.\n// func (t *DeltaTracker[K, V]) Snapshot() map[K]V {\n// \tt.RLock()\n// \tdefer t.RUnlock()\n\n// \tcopyMap := make(map[K]V, len(t.current))\n// \tmaps.Copy(copyMap, t.current)\n// \treturn copyMap\n// }\n\n// Deltas returns a map of all calculated deltas for the current interval.\nfunc (t *DeltaTracker[K, V]) Deltas() map[K]V {\n\tt.RLock()\n\tdefer t.RUnlock()\n\n\tdeltas := make(map[K]V)\n\tfor id, currentVal := range t.current {\n\t\tif previousVal, ok := t.previous[id]; ok {\n\t\t\tdeltas[id] = currentVal - previousVal\n\t\t} else {\n\t\t\tdeltas[id] = 0\n\t\t}\n\t}\n\treturn deltas\n}\n\n// Previous returns the previously recorded value for the given key, if it exists.\nfunc (t *DeltaTracker[K, V]) Previous(id K) (V, bool) {\n\tt.RLock()\n\tdefer t.RUnlock()\n\n\tvalue, ok := t.previous[id]\n\treturn value, ok\n}\n\n// Delta returns the delta for a single key.\n// Returns 0 if the key doesn't exist or has no previous value.\nfunc (t *DeltaTracker[K, V]) Delta(id K) V {\n\tt.RLock()\n\tdefer t.RUnlock()\n\n\tcurrentVal, currentOk := t.current[id]\n\tif !currentOk {\n\t\treturn 0\n\t}\n\n\tpreviousVal, previousOk := t.previous[id]\n\tif !previousOk {\n\t\treturn 0\n\t}\n\n\treturn currentVal - previousVal\n}\n\n// Cycle prepares the tracker for the next interval.\nfunc (t *DeltaTracker[K, V]) Cycle() {\n\tt.Lock()\n\tdefer t.Unlock()\n\tt.previous = t.current\n\tt.current = make(map[K]V)\n}\n"
  },
  {
    "path": "agent/deltatracker/deltatracker_test.go",
    "content": "package deltatracker\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc ExampleDeltaTracker() {\n\ttracker := NewDeltaTracker[string, int]()\n\ttracker.Set(\"key1\", 10)\n\ttracker.Set(\"key2\", 20)\n\ttracker.Cycle()\n\ttracker.Set(\"key1\", 15)\n\ttracker.Set(\"key2\", 30)\n\tfmt.Println(tracker.Delta(\"key1\"))\n\tfmt.Println(tracker.Delta(\"key2\"))\n\tfmt.Println(tracker.Deltas())\n\t// Output: 5\n\t// 10\n\t// map[key1:5 key2:10]\n}\n\nfunc TestNewDeltaTracker(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\tassert.NotNil(t, tracker)\n\tassert.Empty(t, tracker.current)\n\tassert.Empty(t, tracker.previous)\n}\n\nfunc TestSet(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\ttracker.Set(\"key1\", 10)\n\n\ttracker.RLock()\n\tdefer tracker.RUnlock()\n\n\tassert.Equal(t, 10, tracker.current[\"key1\"])\n}\n\nfunc TestDeltas(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\n\t// Test with no previous values\n\ttracker.Set(\"key1\", 10)\n\ttracker.Set(\"key2\", 20)\n\n\tdeltas := tracker.Deltas()\n\tassert.Equal(t, 0, deltas[\"key1\"])\n\tassert.Equal(t, 0, deltas[\"key2\"])\n\n\t// Cycle to move current to previous\n\ttracker.Cycle()\n\n\t// Set new values and check deltas\n\ttracker.Set(\"key1\", 15) // Delta should be 5 (15-10)\n\ttracker.Set(\"key2\", 25) // Delta should be 5 (25-20)\n\ttracker.Set(\"key3\", 30) // New key, delta should be 0\n\n\tdeltas = tracker.Deltas()\n\tassert.Equal(t, 5, deltas[\"key1\"])\n\tassert.Equal(t, 5, deltas[\"key2\"])\n\tassert.Equal(t, 0, deltas[\"key3\"])\n}\n\nfunc TestCycle(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\n\ttracker.Set(\"key1\", 10)\n\ttracker.Set(\"key2\", 20)\n\n\t// Verify current has values\n\ttracker.RLock()\n\tassert.Equal(t, 10, tracker.current[\"key1\"])\n\tassert.Equal(t, 20, tracker.current[\"key2\"])\n\tassert.Empty(t, tracker.previous)\n\ttracker.RUnlock()\n\n\ttracker.Cycle()\n\n\t// After cycle, previous should have the old current values\n\t// and current should be empty\n\ttracker.RLock()\n\tassert.Empty(t, tracker.current)\n\tassert.Equal(t, 10, tracker.previous[\"key1\"])\n\tassert.Equal(t, 20, tracker.previous[\"key2\"])\n\ttracker.RUnlock()\n}\n\nfunc TestCompleteWorkflow(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\n\t// First interval\n\ttracker.Set(\"server1\", 100)\n\ttracker.Set(\"server2\", 200)\n\n\t// Get deltas for first interval (should be zero)\n\tfirstDeltas := tracker.Deltas()\n\tassert.Equal(t, 0, firstDeltas[\"server1\"])\n\tassert.Equal(t, 0, firstDeltas[\"server2\"])\n\n\t// Cycle to next interval\n\ttracker.Cycle()\n\n\t// Second interval\n\ttracker.Set(\"server1\", 150) // Delta: 50\n\ttracker.Set(\"server2\", 180) // Delta: -20\n\ttracker.Set(\"server3\", 300) // New server, delta: 300\n\n\tsecondDeltas := tracker.Deltas()\n\tassert.Equal(t, 50, secondDeltas[\"server1\"])\n\tassert.Equal(t, -20, secondDeltas[\"server2\"])\n\tassert.Equal(t, 0, secondDeltas[\"server3\"])\n}\n\nfunc TestDeltaTrackerWithDifferentTypes(t *testing.T) {\n\t// Test with int64\n\tintTracker := NewDeltaTracker[string, int64]()\n\tintTracker.Set(\"pid1\", 1000)\n\tintTracker.Cycle()\n\tintTracker.Set(\"pid1\", 1200)\n\tintDeltas := intTracker.Deltas()\n\tassert.Equal(t, int64(200), intDeltas[\"pid1\"])\n\n\t// Test with float64\n\tfloatTracker := NewDeltaTracker[string, float64]()\n\tfloatTracker.Set(\"cpu1\", 1.5)\n\tfloatTracker.Cycle()\n\tfloatTracker.Set(\"cpu1\", 2.7)\n\tfloatDeltas := floatTracker.Deltas()\n\tassert.InDelta(t, 1.2, floatDeltas[\"cpu1\"], 0.0001)\n\n\t// Test with int keys\n\tpidTracker := NewDeltaTracker[int, int64]()\n\tpidTracker.Set(101, 20000)\n\tpidTracker.Cycle()\n\tpidTracker.Set(101, 22500)\n\tpidDeltas := pidTracker.Deltas()\n\tassert.Equal(t, int64(2500), pidDeltas[101])\n}\n\nfunc TestDelta(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\n\t// Test getting delta for non-existent key\n\tresult := tracker.Delta(\"nonexistent\")\n\tassert.Equal(t, 0, result)\n\n\t// Test getting delta for key with no previous value\n\ttracker.Set(\"key1\", 10)\n\tresult = tracker.Delta(\"key1\")\n\tassert.Equal(t, 0, result)\n\n\t// Cycle to move current to previous\n\ttracker.Cycle()\n\n\t// Test getting delta for key with previous value\n\ttracker.Set(\"key1\", 15)\n\tresult = tracker.Delta(\"key1\")\n\tassert.Equal(t, 5, result)\n\n\t// Test getting delta for key that exists in previous but not current\n\tresult = tracker.Delta(\"key1\")\n\tassert.Equal(t, 5, result) // Should still return 5\n\n\t// Test getting delta for key that exists in current but not previous\n\ttracker.Set(\"key2\", 20)\n\tresult = tracker.Delta(\"key2\")\n\tassert.Equal(t, 0, result)\n}\n\nfunc TestDeltaWithDifferentTypes(t *testing.T) {\n\t// Test with int64\n\tintTracker := NewDeltaTracker[string, int64]()\n\tintTracker.Set(\"pid1\", 1000)\n\tintTracker.Cycle()\n\tintTracker.Set(\"pid1\", 1200)\n\tresult := intTracker.Delta(\"pid1\")\n\tassert.Equal(t, int64(200), result)\n\n\t// Test with float64\n\tfloatTracker := NewDeltaTracker[string, float64]()\n\tfloatTracker.Set(\"cpu1\", 1.5)\n\tfloatTracker.Cycle()\n\tfloatTracker.Set(\"cpu1\", 2.7)\n\tfloatResult := floatTracker.Delta(\"cpu1\")\n\tassert.InDelta(t, 1.2, floatResult, 0.0001)\n\n\t// Test with int keys\n\tpidTracker := NewDeltaTracker[int, int64]()\n\tpidTracker.Set(101, 20000)\n\tpidTracker.Cycle()\n\tpidTracker.Set(101, 22500)\n\tpidResult := pidTracker.Delta(101)\n\tassert.Equal(t, int64(2500), pidResult)\n}\n\nfunc TestDeltaConcurrentAccess(t *testing.T) {\n\ttracker := NewDeltaTracker[string, int]()\n\n\t// Set initial values\n\ttracker.Set(\"key1\", 10)\n\ttracker.Set(\"key2\", 20)\n\ttracker.Cycle()\n\n\t// Set new values\n\ttracker.Set(\"key1\", 15)\n\ttracker.Set(\"key2\", 25)\n\n\t// Test concurrent access safety\n\tresult1 := tracker.Delta(\"key1\")\n\tresult2 := tracker.Delta(\"key2\")\n\n\tassert.Equal(t, 5, result1)\n\tassert.Equal(t, 5, result2)\n}\n"
  },
  {
    "path": "agent/disk.go",
    "content": "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/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/shirou/gopsutil/v4/disk\"\n)\n\n// fsRegistrationContext holds the shared lookup state needed to resolve a\n// filesystem into the tracked fsStats key and metadata.\ntype fsRegistrationContext struct {\n\tfilesystem     string // value of optional FILESYSTEM env var\n\tisWindows      bool\n\tefPath         string // path to extra filesystems (default \"/extra-filesystems\")\n\tdiskIoCounters map[string]disk.IOCountersStat\n}\n\n// diskDiscovery groups the transient state for a single initializeDiskInfo run so\n// helper methods can share the same partitions, mount paths, and lookup functions\ntype diskDiscovery struct {\n\tagent          *Agent\n\trootMountPoint string\n\tpartitions     []disk.PartitionStat\n\tusageFn        func(string) (*disk.UsageStat, error)\n\tctx            fsRegistrationContext\n}\n\n// parseFilesystemEntry parses a filesystem entry in the format \"device__customname\"\n// Returns the device/filesystem part and the custom name part\nfunc parseFilesystemEntry(entry string) (device, customName string) {\n\tentry = strings.TrimSpace(entry)\n\tif parts := strings.SplitN(entry, \"__\", 2); len(parts) == 2 {\n\t\tdevice = strings.TrimSpace(parts[0])\n\t\tcustomName = strings.TrimSpace(parts[1])\n\t} else {\n\t\tdevice = entry\n\t}\n\treturn device, customName\n}\n\n// extraFilesystemPartitionInfo derives the I/O device and optional display name\n// for a mounted /extra-filesystems partition. Prefer the partition device reported\n// by the system and only use the folder name for custom naming metadata.\nfunc extraFilesystemPartitionInfo(p disk.PartitionStat) (device, customName string) {\n\tdevice = strings.TrimSpace(p.Device)\n\tfolderDevice, customName := parseFilesystemEntry(filepath.Base(p.Mountpoint))\n\tif device == \"\" {\n\t\tdevice = folderDevice\n\t}\n\treturn device, customName\n}\n\nfunc isDockerSpecialMountpoint(mountpoint string) bool {\n\tswitch mountpoint {\n\tcase \"/etc/hosts\", \"/etc/resolv.conf\", \"/etc/hostname\":\n\t\treturn true\n\t}\n\treturn false\n}\n\n// registerFilesystemStats resolves the tracked key and stats payload for a\n// filesystem before it is inserted into fsStats.\nfunc registerFilesystemStats(existing map[string]*system.FsStats, device, mountpoint string, root bool, customName string, ctx fsRegistrationContext) (string, *system.FsStats, bool) {\n\tkey := device\n\tif !ctx.isWindows {\n\t\tkey = filepath.Base(device)\n\t}\n\n\tif root {\n\t\t// Try to map root device to a diskIoCounters entry. First checks for an\n\t\t// exact key match, then uses findIoDevice for normalized / prefix-based\n\t\t// matching (e.g. nda0p2 -> nda0), and finally falls back to FILESYSTEM.\n\t\tif _, ioMatch := ctx.diskIoCounters[key]; !ioMatch {\n\t\t\tif matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match {\n\t\t\t\tkey = matchedKey\n\t\t\t} else if ctx.filesystem != \"\" {\n\t\t\t\tif matchedKey, match := findIoDevice(ctx.filesystem, ctx.diskIoCounters); match {\n\t\t\t\t\tkey = matchedKey\n\t\t\t\t}\n\t\t\t}\n\t\t\tif _, ioMatch = ctx.diskIoCounters[key]; !ioMatch {\n\t\t\t\tslog.Warn(\"Root I/O unmapped; set FILESYSTEM\", \"device\", device, \"mountpoint\", mountpoint)\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Check if non-root has diskstats and prefer the folder device for\n\t\t// /extra-filesystems mounts when the discovered partition device is a\n\t\t// mapper path (e.g. luks UUID) that obscures the underlying block device.\n\t\tif _, ioMatch := ctx.diskIoCounters[key]; !ioMatch {\n\t\t\tif strings.HasPrefix(mountpoint, ctx.efPath) {\n\t\t\t\tfolderDevice, _ := parseFilesystemEntry(filepath.Base(mountpoint))\n\t\t\t\tif folderDevice != \"\" {\n\t\t\t\t\tif matchedKey, match := findIoDevice(folderDevice, ctx.diskIoCounters); match {\n\t\t\t\t\t\tkey = matchedKey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif _, ioMatch = ctx.diskIoCounters[key]; !ioMatch {\n\t\t\t\tif matchedKey, match := findIoDevice(key, ctx.diskIoCounters); match {\n\t\t\t\t\tkey = matchedKey\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, exists := existing[key]; exists {\n\t\treturn \"\", nil, false\n\t}\n\n\tfsStats := &system.FsStats{Root: root, Mountpoint: mountpoint}\n\tif customName != \"\" {\n\t\tfsStats.Name = customName\n\t}\n\treturn key, fsStats, true\n}\n\n// addFsStat inserts a discovered filesystem if it resolves to a new tracking\n// key. The key selection itself lives in buildFsStatRegistration so that logic\n// can stay directly unit-tested.\nfunc (d *diskDiscovery) addFsStat(device, mountpoint string, root bool, customName string) {\n\tkey, fsStats, ok := registerFilesystemStats(d.agent.fsStats, device, mountpoint, root, customName, d.ctx)\n\tif !ok {\n\t\treturn\n\t}\n\td.agent.fsStats[key] = fsStats\n\tname := key\n\tif customName != \"\" {\n\t\tname = customName\n\t}\n\tslog.Info(\"Detected disk\", \"name\", name, \"device\", device, \"mount\", mountpoint, \"io\", key, \"root\", root)\n}\n\n// addConfiguredRootFs resolves FILESYSTEM against partitions first, then falls\n// back to direct diskstats matching for setups like ZFS where partitions do not\n// expose the physical device name.\nfunc (d *diskDiscovery) addConfiguredRootFs() bool {\n\tif d.ctx.filesystem == \"\" {\n\t\treturn false\n\t}\n\n\tfor _, p := range d.partitions {\n\t\tif filesystemMatchesPartitionSetting(d.ctx.filesystem, p) {\n\t\t\td.addFsStat(p.Device, p.Mountpoint, true, \"\")\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// FILESYSTEM may name a physical disk absent from partitions (e.g. ZFS lists\n\t// dataset paths like zroot/ROOT/default, not block devices).\n\tif ioKey, match := findIoDevice(d.ctx.filesystem, d.ctx.diskIoCounters); match {\n\t\td.agent.fsStats[ioKey] = &system.FsStats{Root: true, Mountpoint: d.rootMountPoint}\n\t\treturn true\n\t}\n\n\tslog.Warn(\"Partition details not found\", \"filesystem\", d.ctx.filesystem)\n\treturn false\n}\n\nfunc isRootFallbackPartition(p disk.PartitionStat, rootMountPoint string) bool {\n\treturn p.Mountpoint == rootMountPoint ||\n\t\t(isDockerSpecialMountpoint(p.Mountpoint) && strings.HasPrefix(p.Device, \"/dev\"))\n}\n\n// addPartitionRootFs handles the non-configured root fallback path when a\n// partition looks like the active root mount but still needs translating to an\n// I/O device key.\nfunc (d *diskDiscovery) addPartitionRootFs(device, mountpoint string) bool {\n\tfs, match := findIoDevice(filepath.Base(device), d.ctx.diskIoCounters)\n\tif !match {\n\t\treturn false\n\t}\n\t// The resolved I/O device is already known here, so use it directly to avoid\n\t// a second fallback search inside buildFsStatRegistration.\n\td.addFsStat(fs, mountpoint, true, \"\")\n\treturn true\n}\n\n// addLastResortRootFs is only used when neither FILESYSTEM nor partition-based\n// heuristics can identify root, so it picks the busiest I/O device as a final\n// fallback and preserves the root mountpoint for usage collection.\nfunc (d *diskDiscovery) addLastResortRootFs() {\n\trootKey := mostActiveIoDevice(d.ctx.diskIoCounters)\n\tif rootKey != \"\" {\n\t\tslog.Warn(\"Using most active device for root I/O; set FILESYSTEM to override\", \"device\", rootKey)\n\t} else {\n\t\trootKey = filepath.Base(d.rootMountPoint)\n\t\tif _, exists := d.agent.fsStats[rootKey]; exists {\n\t\t\trootKey = \"root\"\n\t\t}\n\t\tslog.Warn(\"Root I/O device not detected; set FILESYSTEM to override\")\n\t}\n\td.agent.fsStats[rootKey] = &system.FsStats{Root: true, Mountpoint: d.rootMountPoint}\n}\n\n// findPartitionByFilesystemSetting matches an EXTRA_FILESYSTEMS entry against a\n// discovered partition either by mountpoint or by device suffix.\nfunc findPartitionByFilesystemSetting(filesystem string, partitions []disk.PartitionStat) (disk.PartitionStat, bool) {\n\tfor _, p := range partitions {\n\t\tif strings.HasSuffix(p.Device, filesystem) || p.Mountpoint == filesystem {\n\t\t\treturn p, true\n\t\t}\n\t}\n\treturn disk.PartitionStat{}, false\n}\n\n// addConfiguredExtraFsEntry resolves one EXTRA_FILESYSTEMS entry, preferring a\n// discovered partition and falling back to any path that disk.Usage accepts.\nfunc (d *diskDiscovery) addConfiguredExtraFsEntry(filesystem, customName string) {\n\tif p, found := findPartitionByFilesystemSetting(filesystem, d.partitions); found {\n\t\td.addFsStat(p.Device, p.Mountpoint, false, customName)\n\t\treturn\n\t}\n\n\tif _, err := d.usageFn(filesystem); err == nil {\n\t\td.addFsStat(filepath.Base(filesystem), filesystem, false, customName)\n\t\treturn\n\t} else {\n\t\tslog.Error(\"Invalid filesystem\", \"name\", filesystem, \"err\", err)\n\t}\n}\n\n// addConfiguredExtraFilesystems parses and registers the comma-separated\n// EXTRA_FILESYSTEMS env var entries.\nfunc (d *diskDiscovery) addConfiguredExtraFilesystems(extraFilesystems string) {\n\tfor fsEntry := range strings.SplitSeq(extraFilesystems, \",\") {\n\t\tfilesystem, customName := parseFilesystemEntry(fsEntry)\n\t\td.addConfiguredExtraFsEntry(filesystem, customName)\n\t}\n}\n\n// addPartitionExtraFs registers partitions mounted under /extra-filesystems so\n// their display names can come from the folder name while their I/O keys still\n// prefer the underlying partition device.\nfunc (d *diskDiscovery) addPartitionExtraFs(p disk.PartitionStat) {\n\tif !strings.HasPrefix(p.Mountpoint, d.ctx.efPath) {\n\t\treturn\n\t}\n\tdevice, customName := extraFilesystemPartitionInfo(p)\n\td.addFsStat(device, p.Mountpoint, false, customName)\n}\n\n// addExtraFilesystemFolders handles bare directories under /extra-filesystems\n// that may not appear in partition discovery, while skipping mountpoints that\n// were already registered from higher-fidelity sources.\nfunc (d *diskDiscovery) addExtraFilesystemFolders(folderNames []string) {\n\texistingMountpoints := make(map[string]bool, len(d.agent.fsStats))\n\tfor _, stats := range d.agent.fsStats {\n\t\texistingMountpoints[stats.Mountpoint] = true\n\t}\n\n\tfor _, folderName := range folderNames {\n\t\tmountpoint := filepath.Join(d.ctx.efPath, folderName)\n\t\tslog.Debug(\"/extra-filesystems\", \"mountpoint\", mountpoint)\n\t\tif existingMountpoints[mountpoint] {\n\t\t\tcontinue\n\t\t}\n\t\tdevice, customName := parseFilesystemEntry(folderName)\n\t\td.addFsStat(device, mountpoint, false, customName)\n\t}\n}\n\n// Sets up the filesystems to monitor for disk usage and I/O.\nfunc (a *Agent) initializeDiskInfo() {\n\tfilesystem, _ := utils.GetEnv(\"FILESYSTEM\")\n\thasRoot := false\n\tisWindows := runtime.GOOS == \"windows\"\n\n\tpartitions, err := disk.Partitions(false)\n\tif err != nil {\n\t\tslog.Error(\"Error getting disk partitions\", \"err\", err)\n\t}\n\tslog.Debug(\"Disk\", \"partitions\", partitions)\n\n\t// trim trailing backslash for Windows devices (#1361)\n\tif isWindows {\n\t\tfor i, p := range partitions {\n\t\t\tpartitions[i].Device = strings.TrimSuffix(p.Device, \"\\\\\")\n\t\t}\n\t}\n\n\tdiskIoCounters, err := disk.IOCounters()\n\tif err != nil {\n\t\tslog.Error(\"Error getting diskstats\", \"err\", err)\n\t}\n\tslog.Debug(\"Disk I/O\", \"diskstats\", diskIoCounters)\n\tctx := fsRegistrationContext{\n\t\tfilesystem:     filesystem,\n\t\tisWindows:      isWindows,\n\t\tdiskIoCounters: diskIoCounters,\n\t\tefPath:         \"/extra-filesystems\",\n\t}\n\n\t// Get the appropriate root mount point for this system\n\tdiscovery := diskDiscovery{\n\t\tagent:          a,\n\t\trootMountPoint: a.getRootMountPoint(),\n\t\tpartitions:     partitions,\n\t\tusageFn:        disk.Usage,\n\t\tctx:            ctx,\n\t}\n\n\thasRoot = discovery.addConfiguredRootFs()\n\n\t// Add EXTRA_FILESYSTEMS env var values to fsStats\n\tif extraFilesystems, exists := utils.GetEnv(\"EXTRA_FILESYSTEMS\"); exists {\n\t\tdiscovery.addConfiguredExtraFilesystems(extraFilesystems)\n\t}\n\n\t// Process partitions for various mount points\n\tfor _, p := range partitions {\n\t\tif !hasRoot && isRootFallbackPartition(p, discovery.rootMountPoint) {\n\t\t\thasRoot = discovery.addPartitionRootFs(p.Device, p.Mountpoint)\n\t\t}\n\t\tdiscovery.addPartitionExtraFs(p)\n\t}\n\n\t// Check all folders in /extra-filesystems and add them if not already present\n\tif folders, err := os.ReadDir(discovery.ctx.efPath); err == nil {\n\t\tfolderNames := make([]string, 0, len(folders))\n\t\tfor _, folder := range folders {\n\t\t\tif folder.IsDir() {\n\t\t\t\tfolderNames = append(folderNames, folder.Name())\n\t\t\t}\n\t\t}\n\t\tdiscovery.addExtraFilesystemFolders(folderNames)\n\t}\n\n\t// If no root filesystem set, try the most active I/O device as a last\n\t// resort (e.g. ZFS where dataset names are unrelated to disk names).\n\tif !hasRoot {\n\t\tdiscovery.addLastResortRootFs()\n\t}\n\n\ta.pruneDuplicateRootExtraFilesystems()\n\ta.initializeDiskIoStats(diskIoCounters)\n}\n\n// Removes extra filesystems that mirror root usage (https://github.com/henrygd/beszel/issues/1428).\nfunc (a *Agent) pruneDuplicateRootExtraFilesystems() {\n\tvar rootMountpoint string\n\tfor _, stats := range a.fsStats {\n\t\tif stats != nil && stats.Root {\n\t\t\trootMountpoint = stats.Mountpoint\n\t\t\tbreak\n\t\t}\n\t}\n\tif rootMountpoint == \"\" {\n\t\treturn\n\t}\n\trootUsage, err := disk.Usage(rootMountpoint)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor name, stats := range a.fsStats {\n\t\tif stats == nil || stats.Root {\n\t\t\tcontinue\n\t\t}\n\t\textraUsage, err := disk.Usage(stats.Mountpoint)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif hasSameDiskUsage(rootUsage, extraUsage) {\n\t\t\tslog.Info(\"Ignoring duplicate FS\", \"name\", name, \"mount\", stats.Mountpoint)\n\t\t\tdelete(a.fsStats, name)\n\t\t}\n\t}\n}\n\n// hasSameDiskUsage compares root/extra usage with a small byte tolerance.\nfunc hasSameDiskUsage(a, b *disk.UsageStat) bool {\n\tif a == nil || b == nil || a.Total == 0 || b.Total == 0 {\n\t\treturn false\n\t}\n\t// Allow minor drift between sequential disk usage calls.\n\tconst toleranceBytes uint64 = 16 * 1024 * 1024\n\treturn withinUsageTolerance(a.Total, b.Total, toleranceBytes) &&\n\t\twithinUsageTolerance(a.Used, b.Used, toleranceBytes)\n}\n\n// withinUsageTolerance reports whether two byte values differ by at most tolerance.\nfunc withinUsageTolerance(a, b, tolerance uint64) bool {\n\tif a >= b {\n\t\treturn a-b <= tolerance\n\t}\n\treturn b-a <= tolerance\n}\n\ntype ioMatchCandidate struct {\n\tname  string\n\tbytes uint64\n\tops   uint64\n}\n\n// findIoDevice prefers exact device/label matches, then falls back to a\n// prefix-related candidate with the highest recent activity.\nfunc findIoDevice(filesystem string, diskIoCounters map[string]disk.IOCountersStat) (string, bool) {\n\tfilesystem = normalizeDeviceName(filesystem)\n\tif filesystem == \"\" {\n\t\treturn \"\", false\n\t}\n\n\tcandidates := []ioMatchCandidate{}\n\n\tfor _, d := range diskIoCounters {\n\t\tif normalizeDeviceName(d.Name) == filesystem || (d.Label != \"\" && normalizeDeviceName(d.Label) == filesystem) {\n\t\t\treturn d.Name, true\n\t\t}\n\t\tif prefixRelated(normalizeDeviceName(d.Name), filesystem) ||\n\t\t\t(d.Label != \"\" && prefixRelated(normalizeDeviceName(d.Label), filesystem)) {\n\t\t\tcandidates = append(candidates, ioMatchCandidate{\n\t\t\t\tname:  d.Name,\n\t\t\t\tbytes: d.ReadBytes + d.WriteBytes,\n\t\t\t\tops:   d.ReadCount + d.WriteCount,\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(candidates) == 0 {\n\t\treturn \"\", false\n\t}\n\n\tbest := candidates[0]\n\tfor _, c := range candidates[1:] {\n\t\tif c.bytes > best.bytes ||\n\t\t\t(c.bytes == best.bytes && c.ops > best.ops) ||\n\t\t\t(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {\n\t\t\tbest = c\n\t\t}\n\t}\n\n\tslog.Info(\"Using disk I/O fallback\", \"requested\", filesystem, \"selected\", best.name)\n\treturn best.name, true\n}\n\n// mostActiveIoDevice returns the device with the highest I/O activity,\n// or \"\" if diskIoCounters is empty.\nfunc mostActiveIoDevice(diskIoCounters map[string]disk.IOCountersStat) string {\n\tvar best ioMatchCandidate\n\tfor _, d := range diskIoCounters {\n\t\tc := ioMatchCandidate{\n\t\t\tname:  d.Name,\n\t\t\tbytes: d.ReadBytes + d.WriteBytes,\n\t\t\tops:   d.ReadCount + d.WriteCount,\n\t\t}\n\t\tif best.name == \"\" || c.bytes > best.bytes ||\n\t\t\t(c.bytes == best.bytes && c.ops > best.ops) ||\n\t\t\t(c.bytes == best.bytes && c.ops == best.ops && c.name < best.name) {\n\t\t\tbest = c\n\t\t}\n\t}\n\treturn best.name\n}\n\n// prefixRelated reports whether either identifier is a prefix of the other.\nfunc prefixRelated(a, b string) bool {\n\tif a == \"\" || b == \"\" || a == b {\n\t\treturn false\n\t}\n\treturn strings.HasPrefix(a, b) || strings.HasPrefix(b, a)\n}\n\n// filesystemMatchesPartitionSetting checks whether a FILESYSTEM env var value\n// matches a partition by mountpoint, exact device name, or prefix relationship\n// (e.g. FILESYSTEM=ada0 matches partition /dev/ada0p2).\nfunc filesystemMatchesPartitionSetting(filesystem string, p disk.PartitionStat) bool {\n\tfilesystem = strings.TrimSpace(filesystem)\n\tif filesystem == \"\" {\n\t\treturn false\n\t}\n\tif p.Mountpoint == filesystem {\n\t\treturn true\n\t}\n\n\tfsName := normalizeDeviceName(filesystem)\n\tpartName := normalizeDeviceName(p.Device)\n\tif fsName == \"\" || partName == \"\" {\n\t\treturn false\n\t}\n\tif fsName == partName {\n\t\treturn true\n\t}\n\treturn prefixRelated(partName, fsName)\n}\n\n// normalizeDeviceName canonicalizes device strings for comparisons.\nfunc normalizeDeviceName(value string) string {\n\tname := filepath.Base(strings.TrimSpace(value))\n\tif name == \".\" {\n\t\treturn \"\"\n\t}\n\treturn name\n}\n\n// Sets start values for disk I/O stats.\nfunc (a *Agent) initializeDiskIoStats(diskIoCounters map[string]disk.IOCountersStat) {\n\ta.fsNames = a.fsNames[:0]\n\tnow := time.Now()\n\tfor device, stats := range a.fsStats {\n\t\t// skip if not in diskIoCounters\n\t\td, exists := diskIoCounters[device]\n\t\tif !exists {\n\t\t\tslog.Warn(\"Device not found in diskstats\", \"name\", device)\n\t\t\tcontinue\n\t\t}\n\t\t// populate initial values\n\t\tstats.Time = now\n\t\tstats.TotalRead = d.ReadBytes\n\t\tstats.TotalWrite = d.WriteBytes\n\t\t// add to list of valid io device names\n\t\ta.fsNames = append(a.fsNames, device)\n\t}\n}\n\n// Updates disk usage statistics for all monitored filesystems\nfunc (a *Agent) updateDiskUsage(systemStats *system.Stats) {\n\t// Check if we should skip extra filesystem collection to avoid waking sleeping disks.\n\t// Root filesystem is always updated since it can't be sleeping while the agent runs.\n\t// Always collect on first call (lastDiskUsageUpdate is zero) or if caching is disabled.\n\tcacheExtraFs := a.diskUsageCacheDuration > 0 &&\n\t\t!a.lastDiskUsageUpdate.IsZero() &&\n\t\ttime.Since(a.lastDiskUsageUpdate) < a.diskUsageCacheDuration\n\n\t// disk usage\n\tfor _, stats := range a.fsStats {\n\t\t// Skip non-root filesystems if caching is active\n\t\tif cacheExtraFs && !stats.Root {\n\t\t\tcontinue\n\t\t}\n\t\tif d, err := disk.Usage(stats.Mountpoint); err == nil {\n\t\t\tstats.DiskTotal = utils.BytesToGigabytes(d.Total)\n\t\t\tstats.DiskUsed = utils.BytesToGigabytes(d.Used)\n\t\t\tif stats.Root {\n\t\t\t\tsystemStats.DiskTotal = utils.BytesToGigabytes(d.Total)\n\t\t\t\tsystemStats.DiskUsed = utils.BytesToGigabytes(d.Used)\n\t\t\t\tsystemStats.DiskPct = utils.TwoDecimals(d.UsedPercent)\n\t\t\t}\n\t\t} else {\n\t\t\t// reset stats if error (likely unmounted)\n\t\t\tslog.Error(\"Error getting disk stats\", \"name\", stats.Mountpoint, \"err\", err)\n\t\t\tstats.DiskTotal = 0\n\t\t\tstats.DiskUsed = 0\n\t\t\tstats.TotalRead = 0\n\t\t\tstats.TotalWrite = 0\n\t\t}\n\t}\n\n\t// Update the last disk usage update time when we've collected extra filesystems\n\tif !cacheExtraFs {\n\t\ta.lastDiskUsageUpdate = time.Now()\n\t}\n}\n\n// Updates disk I/O statistics for all monitored filesystems\nfunc (a *Agent) updateDiskIo(cacheTimeMs uint16, systemStats *system.Stats) {\n\t// disk i/o (cache-aware per interval)\n\tif ioCounters, err := disk.IOCounters(a.fsNames...); err == nil {\n\t\t// Ensure map for this interval exists\n\t\tif _, ok := a.diskPrev[cacheTimeMs]; !ok {\n\t\t\ta.diskPrev[cacheTimeMs] = make(map[string]prevDisk)\n\t\t}\n\t\tnow := time.Now()\n\t\tfor name, d := range ioCounters {\n\t\t\tstats := a.fsStats[d.Name]\n\t\t\tif stats == nil {\n\t\t\t\t// skip devices not tracked\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Previous snapshot for this interval and device\n\t\t\tprev, hasPrev := a.diskPrev[cacheTimeMs][name]\n\t\t\tif !hasPrev {\n\t\t\t\t// Seed from agent-level fsStats if present, else seed from current\n\t\t\t\tprev = prevDisk{readBytes: stats.TotalRead, writeBytes: stats.TotalWrite, at: stats.Time}\n\t\t\t\tif prev.at.IsZero() {\n\t\t\t\t\tprev = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmsElapsed := uint64(now.Sub(prev.at).Milliseconds())\n\t\t\tif msElapsed < 100 {\n\t\t\t\t// Avoid division by zero or clock issues; update snapshot and continue\n\t\t\t\ta.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdiskIORead := (d.ReadBytes - prev.readBytes) * 1000 / msElapsed\n\t\t\tdiskIOWrite := (d.WriteBytes - prev.writeBytes) * 1000 / msElapsed\n\t\t\treadMbPerSecond := utils.BytesToMegabytes(float64(diskIORead))\n\t\t\twriteMbPerSecond := utils.BytesToMegabytes(float64(diskIOWrite))\n\n\t\t\t// validate values\n\t\t\tif readMbPerSecond > 50_000 || writeMbPerSecond > 50_000 {\n\t\t\t\tslog.Warn(\"Invalid disk I/O. Resetting.\", \"name\", d.Name, \"read\", readMbPerSecond, \"write\", writeMbPerSecond)\n\t\t\t\t// Reset interval snapshot and seed from current\n\t\t\t\ta.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}\n\t\t\t\t// also refresh agent baseline to avoid future negatives\n\t\t\t\ta.initializeDiskIoStats(ioCounters)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Update per-interval snapshot\n\t\t\ta.diskPrev[cacheTimeMs][name] = prevDisk{readBytes: d.ReadBytes, writeBytes: d.WriteBytes, at: now}\n\n\t\t\t// Update global fsStats baseline for cross-interval correctness\n\t\t\tstats.Time = now\n\t\t\tstats.TotalRead = d.ReadBytes\n\t\t\tstats.TotalWrite = d.WriteBytes\n\t\t\tstats.DiskReadPs = readMbPerSecond\n\t\t\tstats.DiskWritePs = writeMbPerSecond\n\t\t\tstats.DiskReadBytes = diskIORead\n\t\t\tstats.DiskWriteBytes = diskIOWrite\n\n\t\t\tif stats.Root {\n\t\t\t\tsystemStats.DiskReadPs = stats.DiskReadPs\n\t\t\t\tsystemStats.DiskWritePs = stats.DiskWritePs\n\t\t\t\tsystemStats.DiskIO[0] = diskIORead\n\t\t\t\tsystemStats.DiskIO[1] = diskIOWrite\n\t\t\t}\n\t\t}\n\t}\n}\n\n// getRootMountPoint returns the appropriate root mount point for the system\n// For immutable systems like Fedora Silverblue, it returns /sysroot instead of /\nfunc (a *Agent) getRootMountPoint() string {\n\t// 1. Check if /etc/os-release contains indicators of an immutable system\n\tif osReleaseContent, err := os.ReadFile(\"/etc/os-release\"); err == nil {\n\t\tcontent := string(osReleaseContent)\n\t\tif strings.Contains(content, \"fedora\") && strings.Contains(content, \"silverblue\") ||\n\t\t\tstrings.Contains(content, \"coreos\") ||\n\t\t\tstrings.Contains(content, \"flatcar\") ||\n\t\t\tstrings.Contains(content, \"rhel-atomic\") ||\n\t\t\tstrings.Contains(content, \"centos-atomic\") {\n\t\t\t// Verify that /sysroot exists before returning it\n\t\t\tif _, err := os.Stat(\"/sysroot\"); err == nil {\n\t\t\t\treturn \"/sysroot\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Check if /run/ostree is present (ostree-based systems like Silverblue)\n\tif _, err := os.Stat(\"/run/ostree\"); err == nil {\n\t\t// Verify that /sysroot exists before returning it\n\t\tif _, err := os.Stat(\"/sysroot\"); err == nil {\n\t\t\treturn \"/sysroot\"\n\t\t}\n\t}\n\n\treturn \"/\"\n}\n"
  },
  {
    "path": "agent/disk_test.go",
    "content": "//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/entities/system\"\n\t\"github.com/shirou/gopsutil/v4/disk\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseFilesystemEntry(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tinput        string\n\t\texpectedFs   string\n\t\texpectedName string\n\t}{\n\t\t{\n\t\t\tname:         \"simple device name\",\n\t\t\tinput:        \"sda1\",\n\t\t\texpectedFs:   \"sda1\",\n\t\t\texpectedName: \"\",\n\t\t},\n\t\t{\n\t\t\tname:         \"device with custom name\",\n\t\t\tinput:        \"sda1__my-storage\",\n\t\t\texpectedFs:   \"sda1\",\n\t\t\texpectedName: \"my-storage\",\n\t\t},\n\t\t{\n\t\t\tname:         \"full device path with custom name\",\n\t\t\tinput:        \"/dev/sdb1__backup-drive\",\n\t\t\texpectedFs:   \"/dev/sdb1\",\n\t\t\texpectedName: \"backup-drive\",\n\t\t},\n\t\t{\n\t\t\tname:         \"NVMe device with custom name\",\n\t\t\tinput:        \"nvme0n1p2__fast-ssd\",\n\t\t\texpectedFs:   \"nvme0n1p2\",\n\t\t\texpectedName: \"fast-ssd\",\n\t\t},\n\t\t{\n\t\t\tname:         \"whitespace trimmed\",\n\t\t\tinput:        \"  sda2__trimmed-name  \",\n\t\t\texpectedFs:   \"sda2\",\n\t\t\texpectedName: \"trimmed-name\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty custom name\",\n\t\t\tinput:        \"sda3__\",\n\t\t\texpectedFs:   \"sda3\",\n\t\t\texpectedName: \"\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty device name\",\n\t\t\tinput:        \"__just-custom\",\n\t\t\texpectedFs:   \"\",\n\t\t\texpectedName: \"just-custom\",\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple underscores in custom name\",\n\t\t\tinput:        \"sda1__my_custom_drive\",\n\t\t\texpectedFs:   \"sda1\",\n\t\t\texpectedName: \"my_custom_drive\",\n\t\t},\n\t\t{\n\t\t\tname:         \"custom name with spaces\",\n\t\t\tinput:        \"sda1__My Storage Drive\",\n\t\t\texpectedFs:   \"sda1\",\n\t\t\texpectedName: \"My Storage Drive\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfsEntry := strings.TrimSpace(tt.input)\n\t\t\tvar fs, customName string\n\t\t\tif parts := strings.SplitN(fsEntry, \"__\", 2); len(parts) == 2 {\n\t\t\t\tfs = strings.TrimSpace(parts[0])\n\t\t\t\tcustomName = strings.TrimSpace(parts[1])\n\t\t\t} else {\n\t\t\t\tfs = fsEntry\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedFs, fs)\n\t\t\tassert.Equal(t, tt.expectedName, customName)\n\t\t})\n\t}\n}\n\nfunc TestExtraFilesystemPartitionInfo(t *testing.T) {\n\tt.Run(\"uses partition device for label-only mountpoint\", func(t *testing.T) {\n\t\tdevice, customName := extraFilesystemPartitionInfo(disk.PartitionStat{\n\t\t\tDevice:     \"/dev/sdc\",\n\t\t\tMountpoint: \"/extra-filesystems/Share\",\n\t\t})\n\n\t\tassert.Equal(t, \"/dev/sdc\", device)\n\t\tassert.Equal(t, \"\", customName)\n\t})\n\n\tt.Run(\"uses custom name from mountpoint suffix\", func(t *testing.T) {\n\t\tdevice, customName := extraFilesystemPartitionInfo(disk.PartitionStat{\n\t\t\tDevice:     \"/dev/sdc\",\n\t\t\tMountpoint: \"/extra-filesystems/sdc__Share\",\n\t\t})\n\n\t\tassert.Equal(t, \"/dev/sdc\", device)\n\t\tassert.Equal(t, \"Share\", customName)\n\t})\n\n\tt.Run(\"falls back to folder device when partition device is unavailable\", func(t *testing.T) {\n\t\tdevice, customName := extraFilesystemPartitionInfo(disk.PartitionStat{\n\t\t\tMountpoint: \"/extra-filesystems/sdc__Share\",\n\t\t})\n\n\t\tassert.Equal(t, \"sdc\", device)\n\t\tassert.Equal(t, \"Share\", customName)\n\t})\n\n\tt.Run(\"supports custom name without folder device prefix\", func(t *testing.T) {\n\t\tdevice, customName := extraFilesystemPartitionInfo(disk.PartitionStat{\n\t\t\tDevice:     \"/dev/sdc\",\n\t\t\tMountpoint: \"/extra-filesystems/__Share\",\n\t\t})\n\n\t\tassert.Equal(t, \"/dev/sdc\", device)\n\t\tassert.Equal(t, \"Share\", customName)\n\t})\n}\n\nfunc TestBuildFsStatRegistration(t *testing.T) {\n\tt.Run(\"uses basename for non-windows exact io match\", func(t *testing.T) {\n\t\tkey, stats, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t\"/dev/sda1\",\n\t\t\t\"/mnt/data\",\n\t\t\tfalse,\n\t\t\t\"archive\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"sda1\": {Name: \"sda1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sda1\", key)\n\t\tassert.Equal(t, \"/mnt/data\", stats.Mountpoint)\n\t\tassert.Equal(t, \"archive\", stats.Name)\n\t\tassert.False(t, stats.Root)\n\t})\n\n\tt.Run(\"maps root partition to io device by prefix\", func(t *testing.T) {\n\t\tkey, stats, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t\"/dev/ada0p2\",\n\t\t\t\"/\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"ada0\": {Name: \"ada0\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"ada0\", key)\n\t\tassert.True(t, stats.Root)\n\t\tassert.Equal(t, \"/\", stats.Mountpoint)\n\t})\n\n\tt.Run(\"uses filesystem setting as root fallback\", func(t *testing.T) {\n\t\tkey, _, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t\"overlay\",\n\t\t\t\"/\",\n\t\t\ttrue,\n\t\t\t\"\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tfilesystem: \"nvme0n1p2\",\n\t\t\t\tisWindows:  false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"nvme0n1\": {Name: \"nvme0n1\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"nvme0n1\", key)\n\t})\n\n\tt.Run(\"prefers parsed extra-filesystems device over mapper device\", func(t *testing.T) {\n\t\tkey, stats, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t\"/dev/mapper/luks-2bcb02be-999d-4417-8d18-5c61e660fb6e\",\n\t\t\t\"/extra-filesystems/nvme0n1p2__Archive\",\n\t\t\tfalse,\n\t\t\t\"Archive\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"dm-1\":      {Name: \"dm-1\", Label: \"luks-2bcb02be-999d-4417-8d18-5c61e660fb6e\"},\n\t\t\t\t\t\"nvme0n1p2\": {Name: \"nvme0n1p2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"nvme0n1p2\", key)\n\t\tassert.Equal(t, \"Archive\", stats.Name)\n\t})\n\n\tt.Run(\"falls back to mapper io device when folder device cannot be resolved\", func(t *testing.T) {\n\t\tkey, stats, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t\"/dev/mapper/luks-2bcb02be-999d-4417-8d18-5c61e660fb6e\",\n\t\t\t\"/extra-filesystems/Archive\",\n\t\t\tfalse,\n\t\t\t\"Archive\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"dm-1\": {Name: \"dm-1\", Label: \"luks-2bcb02be-999d-4417-8d18-5c61e660fb6e\"},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"dm-1\", key)\n\t\tassert.Equal(t, \"Archive\", stats.Name)\n\t})\n\n\tt.Run(\"uses full device name on windows\", func(t *testing.T) {\n\t\tkey, _, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{},\n\t\t\t`C:`,\n\t\t\t`C:\\\\`,\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: true,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t`C:`: {Name: `C:`},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, `C:`, key)\n\t})\n\n\tt.Run(\"skips existing key\", func(t *testing.T) {\n\t\tkey, stats, ok := registerFilesystemStats(\n\t\t\tmap[string]*system.FsStats{\"sda1\": {Mountpoint: \"/existing\"}},\n\t\t\t\"/dev/sda1\",\n\t\t\t\"/mnt/data\",\n\t\t\tfalse,\n\t\t\t\"\",\n\t\t\tfsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"sda1\": {Name: \"sda1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t)\n\n\t\tassert.False(t, ok)\n\t\tassert.Empty(t, key)\n\t\tassert.Nil(t, stats)\n\t})\n}\n\nfunc TestAddConfiguredRootFs(t *testing.T) {\n\tt.Run(\"adds root from matching partition\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent:          agent,\n\t\t\trootMountPoint: \"/\",\n\t\t\tpartitions:     []disk.PartitionStat{{Device: \"/dev/ada0p2\", Mountpoint: \"/\"}},\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tfilesystem: \"/dev/ada0p2\",\n\t\t\t\tisWindows:  false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"ada0\": {Name: \"ada0\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tok := discovery.addConfiguredRootFs()\n\n\t\tassert.True(t, ok)\n\t\tstats, exists := agent.fsStats[\"ada0\"]\n\t\tassert.True(t, exists)\n\t\tassert.True(t, stats.Root)\n\t\tassert.Equal(t, \"/\", stats.Mountpoint)\n\t})\n\n\tt.Run(\"adds root from io device when partition is missing\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent:          agent,\n\t\t\trootMountPoint: \"/sysroot\",\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tfilesystem: \"zroot\",\n\t\t\t\tisWindows:  false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"nda0\": {Name: \"nda0\", Label: \"zroot\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tok := discovery.addConfiguredRootFs()\n\n\t\tassert.True(t, ok)\n\t\tstats, exists := agent.fsStats[\"nda0\"]\n\t\tassert.True(t, exists)\n\t\tassert.True(t, stats.Root)\n\t\tassert.Equal(t, \"/sysroot\", stats.Mountpoint)\n\t})\n\n\tt.Run(\"returns false when filesystem cannot be resolved\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent:          agent,\n\t\t\trootMountPoint: \"/\",\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tfilesystem:     \"missing-disk\",\n\t\t\t\tisWindows:      false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{},\n\t\t\t},\n\t\t}\n\n\t\tok := discovery.addConfiguredRootFs()\n\n\t\tassert.False(t, ok)\n\t\tassert.Empty(t, agent.fsStats)\n\t})\n}\n\nfunc TestAddPartitionRootFs(t *testing.T) {\n\tt.Run(\"adds root from fallback partition candidate\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent: agent,\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"nvme0n1\": {Name: \"nvme0n1\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tok := discovery.addPartitionRootFs(\"/dev/nvme0n1p2\", \"/\")\n\n\t\tassert.True(t, ok)\n\t\tstats, exists := agent.fsStats[\"nvme0n1\"]\n\t\tassert.True(t, exists)\n\t\tassert.True(t, stats.Root)\n\t\tassert.Equal(t, \"/\", stats.Mountpoint)\n\t})\n\n\tt.Run(\"returns false when no io device matches\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{agent: agent, ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{}}}\n\n\t\tok := discovery.addPartitionRootFs(\"/dev/mapper/root\", \"/\")\n\n\t\tassert.False(t, ok)\n\t\tassert.Empty(t, agent.fsStats)\n\t})\n}\n\nfunc TestAddLastResortRootFs(t *testing.T) {\n\tt.Run(\"uses most active io device when available\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{agent: agent, rootMountPoint: \"/\", ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\", ReadBytes: 5000, WriteBytes: 5000},\n\t\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t}}}\n\n\t\tdiscovery.addLastResortRootFs()\n\n\t\tstats, exists := agent.fsStats[\"sda\"]\n\t\tassert.True(t, exists)\n\t\tassert.True(t, stats.Root)\n\t})\n\n\tt.Run(\"falls back to root key when mountpoint basename collides\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: map[string]*system.FsStats{\n\t\t\t\"sysroot\": {Mountpoint: \"/extra-filesystems/sysroot\"},\n\t\t}}\n\t\tdiscovery := diskDiscovery{agent: agent, rootMountPoint: \"/sysroot\", ctx: fsRegistrationContext{diskIoCounters: map[string]disk.IOCountersStat{}}}\n\n\t\tdiscovery.addLastResortRootFs()\n\n\t\tstats, exists := agent.fsStats[\"root\"]\n\t\tassert.True(t, exists)\n\t\tassert.True(t, stats.Root)\n\t\tassert.Equal(t, \"/sysroot\", stats.Mountpoint)\n\t})\n}\n\nfunc TestAddConfiguredExtraFsEntry(t *testing.T) {\n\tt.Run(\"uses matching partition when present\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent:      agent,\n\t\t\tpartitions: []disk.PartitionStat{{Device: \"/dev/sdb1\", Mountpoint: \"/mnt/backup\"}},\n\t\t\tusageFn: func(string) (*disk.UsageStat, error) {\n\t\t\t\tt.Fatal(\"usage fallback should not be called when partition matches\")\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"sdb1\": {Name: \"sdb1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tdiscovery.addConfiguredExtraFsEntry(\"sdb1\", \"backup\")\n\n\t\tstats, exists := agent.fsStats[\"sdb1\"]\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, \"/mnt/backup\", stats.Mountpoint)\n\t\tassert.Equal(t, \"backup\", stats.Name)\n\t})\n\n\tt.Run(\"falls back to usage-validated path\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent: agent,\n\t\t\tusageFn: func(path string) (*disk.UsageStat, error) {\n\t\t\t\tassert.Equal(t, \"/srv/archive\", path)\n\t\t\t\treturn &disk.UsageStat{}, nil\n\t\t\t},\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"archive\": {Name: \"archive\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tdiscovery.addConfiguredExtraFsEntry(\"/srv/archive\", \"archive\")\n\n\t\tstats, exists := agent.fsStats[\"archive\"]\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, \"/srv/archive\", stats.Mountpoint)\n\t\tassert.Equal(t, \"archive\", stats.Name)\n\t})\n\n\tt.Run(\"ignores invalid filesystem entry\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent: agent,\n\t\t\tusageFn: func(string) (*disk.UsageStat, error) {\n\t\t\t\treturn nil, os.ErrNotExist\n\t\t\t},\n\t\t}\n\n\t\tdiscovery.addConfiguredExtraFsEntry(\"/missing/archive\", \"\")\n\n\t\tassert.Empty(t, agent.fsStats)\n\t})\n}\n\nfunc TestAddConfiguredExtraFilesystems(t *testing.T) {\n\tt.Run(\"parses and registers multiple configured filesystems\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: make(map[string]*system.FsStats)}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent:      agent,\n\t\t\tpartitions: []disk.PartitionStat{{Device: \"/dev/sda1\", Mountpoint: \"/mnt/fast\"}},\n\t\t\tusageFn: func(path string) (*disk.UsageStat, error) {\n\t\t\t\tif path == \"/srv/archive\" {\n\t\t\t\t\treturn &disk.UsageStat{}, nil\n\t\t\t\t}\n\t\t\t\treturn nil, os.ErrNotExist\n\t\t\t},\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"sda1\":    {Name: \"sda1\"},\n\t\t\t\t\t\"archive\": {Name: \"archive\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tdiscovery.addConfiguredExtraFilesystems(\"sda1__fast,/srv/archive__cold\")\n\n\t\tassert.Contains(t, agent.fsStats, \"sda1\")\n\t\tassert.Equal(t, \"fast\", agent.fsStats[\"sda1\"].Name)\n\t\tassert.Contains(t, agent.fsStats, \"archive\")\n\t\tassert.Equal(t, \"cold\", agent.fsStats[\"archive\"].Name)\n\t})\n}\n\nfunc TestAddExtraFilesystemFolders(t *testing.T) {\n\tt.Run(\"adds missing folders and skips existing mountpoints\", func(t *testing.T) {\n\t\tagent := &Agent{fsStats: map[string]*system.FsStats{\n\t\t\t\"existing\": {Mountpoint: \"/extra-filesystems/existing\"},\n\t\t}}\n\t\tdiscovery := diskDiscovery{\n\t\t\tagent: agent,\n\t\t\tctx: fsRegistrationContext{\n\t\t\t\tisWindows: false,\n\t\t\t\tefPath:    \"/extra-filesystems\",\n\t\t\t\tdiskIoCounters: map[string]disk.IOCountersStat{\n\t\t\t\t\t\"newdisk\": {Name: \"newdisk\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tdiscovery.addExtraFilesystemFolders([]string{\"existing\", \"newdisk__Archive\"})\n\n\t\tassert.Len(t, agent.fsStats, 2)\n\t\tstats, exists := agent.fsStats[\"newdisk\"]\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, \"/extra-filesystems/newdisk__Archive\", stats.Mountpoint)\n\t\tassert.Equal(t, \"Archive\", stats.Name)\n\t})\n}\n\nfunc TestFindIoDevice(t *testing.T) {\n\tt.Run(\"matches by device name\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\"},\n\t\t\t\"sdb\": {Name: \"sdb\"},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"sdb\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sdb\", device)\n\t})\n\n\tt.Run(\"matches by device label\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\", Label: \"rootfs\"},\n\t\t\t\"sdb\": {Name: \"sdb\"},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"rootfs\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sda\", device)\n\t})\n\n\tt.Run(\"returns no match when not found\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\"},\n\t\t\t\"sdb\": {Name: \"sdb\"},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"nvme0n1p1\", ioCounters)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, \"\", device)\n\t})\n\n\tt.Run(\"uses uncertain unique prefix fallback\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"nvme0n1\": {Name: \"nvme0n1\"},\n\t\t\t\"sda\":     {Name: \"sda\"},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"nvme0n1p2\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"nvme0n1\", device)\n\t})\n\n\tt.Run(\"uses dominant activity when prefix matches are ambiguous\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},\n\t\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"sd\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sda\", device)\n\t})\n\n\tt.Run(\"uses highest activity when ambiguous without dominance\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sda\": {Name: \"sda\", ReadBytes: 3000, WriteBytes: 3000, ReadCount: 50, WriteCount: 50},\n\t\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 2500, WriteBytes: 2500, ReadCount: 40, WriteCount: 40},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"sd\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sda\", device)\n\t})\n\n\tt.Run(\"matches /dev/-prefixed partition to parent disk\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"nda0\": {Name: \"nda0\", ReadBytes: 1000, WriteBytes: 1000},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"/dev/nda0p2\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"nda0\", device)\n\t})\n\n\tt.Run(\"uses deterministic name tie-breaker\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},\n\t\t\t\"sda\": {Name: \"sda\", ReadBytes: 2000, WriteBytes: 2000, ReadCount: 10, WriteCount: 10},\n\t\t}\n\n\t\tdevice, ok := findIoDevice(\"sd\", ioCounters)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, \"sda\", device)\n\t})\n}\n\nfunc TestFilesystemMatchesPartitionSetting(t *testing.T) {\n\tp := disk.PartitionStat{Device: \"/dev/ada0p2\", Mountpoint: \"/\"}\n\n\tt.Run(\"matches mountpoint setting\", func(t *testing.T) {\n\t\tassert.True(t, filesystemMatchesPartitionSetting(\"/\", p))\n\t})\n\n\tt.Run(\"matches exact partition setting\", func(t *testing.T) {\n\t\tassert.True(t, filesystemMatchesPartitionSetting(\"ada0p2\", p))\n\t\tassert.True(t, filesystemMatchesPartitionSetting(\"/dev/ada0p2\", p))\n\t})\n\n\tt.Run(\"matches prefix-style parent setting\", func(t *testing.T) {\n\t\tassert.True(t, filesystemMatchesPartitionSetting(\"ada0\", p))\n\t\tassert.True(t, filesystemMatchesPartitionSetting(\"/dev/ada0\", p))\n\t})\n\n\tt.Run(\"does not match unrelated device\", func(t *testing.T) {\n\t\tassert.False(t, filesystemMatchesPartitionSetting(\"sda\", p))\n\t\tassert.False(t, filesystemMatchesPartitionSetting(\"nvme0n1\", p))\n\t\tassert.False(t, filesystemMatchesPartitionSetting(\"\", p))\n\t})\n}\n\nfunc TestMostActiveIoDevice(t *testing.T) {\n\tt.Run(\"returns most active device\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"nda0\": {Name: \"nda0\", ReadBytes: 5000, WriteBytes: 5000, ReadCount: 100, WriteCount: 100},\n\t\t\t\"nda1\": {Name: \"nda1\", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 50, WriteCount: 50},\n\t\t}\n\t\tassert.Equal(t, \"nda0\", mostActiveIoDevice(ioCounters))\n\t})\n\n\tt.Run(\"uses deterministic tie-breaker\", func(t *testing.T) {\n\t\tioCounters := map[string]disk.IOCountersStat{\n\t\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},\n\t\t\t\"sda\": {Name: \"sda\", ReadBytes: 1000, WriteBytes: 1000, ReadCount: 10, WriteCount: 10},\n\t\t}\n\t\tassert.Equal(t, \"sda\", mostActiveIoDevice(ioCounters))\n\t})\n\n\tt.Run(\"returns empty for empty map\", func(t *testing.T) {\n\t\tassert.Equal(t, \"\", mostActiveIoDevice(map[string]disk.IOCountersStat{}))\n\t})\n}\n\nfunc TestIsDockerSpecialMountpoint(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\tmountpoint string\n\t\texpected   bool\n\t}{\n\t\t{name: \"hosts\", mountpoint: \"/etc/hosts\", expected: true},\n\t\t{name: \"resolv\", mountpoint: \"/etc/resolv.conf\", expected: true},\n\t\t{name: \"hostname\", mountpoint: \"/etc/hostname\", expected: true},\n\t\t{name: \"root\", mountpoint: \"/\", expected: false},\n\t\t{name: \"passwd\", mountpoint: \"/etc/passwd\", expected: false},\n\t\t{name: \"extra-filesystem\", mountpoint: \"/extra-filesystems/sda1\", expected: false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.expected, isDockerSpecialMountpoint(tc.mountpoint))\n\t\t})\n\t}\n}\n\nfunc TestInitializeDiskInfoWithCustomNames(t *testing.T) {\n\t// Set up environment variables\n\toldEnv := os.Getenv(\"EXTRA_FILESYSTEMS\")\n\tdefer func() {\n\t\tif oldEnv != \"\" {\n\t\t\tos.Setenv(\"EXTRA_FILESYSTEMS\", oldEnv)\n\t\t} else {\n\t\t\tos.Unsetenv(\"EXTRA_FILESYSTEMS\")\n\t\t}\n\t}()\n\n\t// Test with custom names\n\tos.Setenv(\"EXTRA_FILESYSTEMS\", \"sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2\")\n\n\t// Mock disk partitions (we'll just test the parsing logic)\n\t// Since the actual disk operations are system-dependent, we'll focus on the parsing\n\ttestCases := []struct {\n\t\tenvValue      string\n\t\texpectedFs    []string\n\t\texpectedNames map[string]string\n\t}{\n\t\t{\n\t\t\tenvValue:   \"sda1__my-storage,sdb1__backup-drive\",\n\t\t\texpectedFs: []string{\"sda1\", \"sdb1\"},\n\t\t\texpectedNames: map[string]string{\n\t\t\t\t\"sda1\": \"my-storage\",\n\t\t\t\t\"sdb1\": \"backup-drive\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tenvValue:   \"sda1,nvme0n1p2__fast-ssd\",\n\t\t\texpectedFs: []string{\"sda1\", \"nvme0n1p2\"},\n\t\t\texpectedNames: map[string]string{\n\t\t\t\t\"nvme0n1p2\": \"fast-ssd\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(\"env_\"+tc.envValue, func(t *testing.T) {\n\t\t\tos.Setenv(\"EXTRA_FILESYSTEMS\", tc.envValue)\n\n\t\t\t// Create mock partitions that would match our test cases\n\t\t\tpartitions := []disk.PartitionStat{}\n\t\t\tfor _, fs := range tc.expectedFs {\n\t\t\t\tif strings.HasPrefix(fs, \"/dev/\") {\n\t\t\t\t\tpartitions = append(partitions, disk.PartitionStat{\n\t\t\t\t\t\tDevice:     fs,\n\t\t\t\t\t\tMountpoint: fs,\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tpartitions = append(partitions, disk.PartitionStat{\n\t\t\t\t\t\tDevice:     \"/dev/\" + fs,\n\t\t\t\t\t\tMountpoint: \"/\" + fs,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test the parsing logic by calling the relevant part\n\t\t\t// We'll create a simplified version to test just the parsing\n\t\t\textraFilesystems := tc.envValue\n\t\t\tfor fsEntry := range strings.SplitSeq(extraFilesystems, \",\") {\n\t\t\t\t// Parse the entry\n\t\t\t\tfsEntry = strings.TrimSpace(fsEntry)\n\t\t\t\tvar fs, customName string\n\t\t\t\tif parts := strings.SplitN(fsEntry, \"__\", 2); len(parts) == 2 {\n\t\t\t\t\tfs = strings.TrimSpace(parts[0])\n\t\t\t\t\tcustomName = strings.TrimSpace(parts[1])\n\t\t\t\t} else {\n\t\t\t\t\tfs = fsEntry\n\t\t\t\t}\n\n\t\t\t\t// Verify the device is in our expected list\n\t\t\t\tassert.Contains(t, tc.expectedFs, fs, \"parsed device should be in expected list\")\n\n\t\t\t\t// Check if custom name should exist\n\t\t\t\tif expectedName, exists := tc.expectedNames[fs]; exists {\n\t\t\t\t\tassert.Equal(t, expectedName, customName, \"custom name should match expected\")\n\t\t\t\t} else {\n\t\t\t\t\tassert.Empty(t, customName, \"custom name should be empty when not expected\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFsStatsWithCustomNames(t *testing.T) {\n\t// Test that FsStats properly stores custom names\n\tfsStats := &system.FsStats{\n\t\tMountpoint: \"/mnt/storage\",\n\t\tName:       \"my-custom-storage\",\n\t\tDiskTotal:  100.0,\n\t\tDiskUsed:   50.0,\n\t}\n\n\tassert.Equal(t, \"my-custom-storage\", fsStats.Name)\n\tassert.Equal(t, \"/mnt/storage\", fsStats.Mountpoint)\n\tassert.Equal(t, 100.0, fsStats.DiskTotal)\n\tassert.Equal(t, 50.0, fsStats.DiskUsed)\n}\n\nfunc TestExtraFsKeyGeneration(t *testing.T) {\n\t// Test the logic for generating ExtraFs keys with custom names\n\ttestCases := []struct {\n\t\tname        string\n\t\tdeviceName  string\n\t\tcustomName  string\n\t\texpectedKey string\n\t}{\n\t\t{\n\t\t\tname:        \"with custom name\",\n\t\t\tdeviceName:  \"sda1\",\n\t\t\tcustomName:  \"my-storage\",\n\t\t\texpectedKey: \"my-storage\",\n\t\t},\n\t\t{\n\t\t\tname:        \"without custom name\",\n\t\t\tdeviceName:  \"sda1\",\n\t\t\tcustomName:  \"\",\n\t\t\texpectedKey: \"sda1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty custom name falls back to device\",\n\t\t\tdeviceName:  \"nvme0n1p2\",\n\t\t\tcustomName:  \"\",\n\t\t\texpectedKey: \"nvme0n1p2\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Simulate the key generation logic from agent.go\n\t\t\tkey := tc.deviceName\n\t\t\tif tc.customName != \"\" {\n\t\t\t\tkey = tc.customName\n\t\t\t}\n\t\t\tassert.Equal(t, tc.expectedKey, key)\n\t\t})\n\t}\n}\n\nfunc TestDiskUsageCaching(t *testing.T) {\n\tt.Run(\"caching disabled updates all filesystems\", func(t *testing.T) {\n\t\tagent := &Agent{\n\t\t\tfsStats: map[string]*system.FsStats{\n\t\t\t\t\"sda\": {Root: true, Mountpoint: \"/\"},\n\t\t\t\t\"sdb\": {Root: false, Mountpoint: \"/mnt/storage\"},\n\t\t\t},\n\t\t\tdiskUsageCacheDuration: 0, // caching disabled\n\t\t}\n\n\t\tvar stats system.Stats\n\t\tagent.updateDiskUsage(&stats)\n\n\t\t// Both should be updated (non-zero values from disk.Usage)\n\t\t// Root stats should be populated in systemStats\n\t\tassert.True(t, agent.lastDiskUsageUpdate.IsZero() || !agent.lastDiskUsageUpdate.IsZero(),\n\t\t\t\"lastDiskUsageUpdate should be set when caching is disabled\")\n\t})\n\n\tt.Run(\"caching enabled always updates root filesystem\", func(t *testing.T) {\n\t\tagent := &Agent{\n\t\t\tfsStats: map[string]*system.FsStats{\n\t\t\t\t\"sda\": {Root: true, Mountpoint: \"/\", DiskTotal: 100, DiskUsed: 50},\n\t\t\t\t\"sdb\": {Root: false, Mountpoint: \"/mnt/storage\", DiskTotal: 200, DiskUsed: 100},\n\t\t\t},\n\t\t\tdiskUsageCacheDuration: 1 * time.Hour,\n\t\t\tlastDiskUsageUpdate:    time.Now(), // cache is fresh\n\t\t}\n\n\t\t// Store original extra fs values\n\t\toriginalExtraTotal := agent.fsStats[\"sdb\"].DiskTotal\n\t\toriginalExtraUsed := agent.fsStats[\"sdb\"].DiskUsed\n\n\t\tvar stats system.Stats\n\t\tagent.updateDiskUsage(&stats)\n\n\t\t// Root should be updated (systemStats populated from disk.Usage call)\n\t\t// We can't easily check if disk.Usage was called, but we verify the flow works\n\n\t\t// Extra filesystem should retain cached values (not reset)\n\t\tassert.Equal(t, originalExtraTotal, agent.fsStats[\"sdb\"].DiskTotal,\n\t\t\t\"extra filesystem DiskTotal should be unchanged when cached\")\n\t\tassert.Equal(t, originalExtraUsed, agent.fsStats[\"sdb\"].DiskUsed,\n\t\t\t\"extra filesystem DiskUsed should be unchanged when cached\")\n\t})\n\n\tt.Run(\"first call always updates all filesystems\", func(t *testing.T) {\n\t\tagent := &Agent{\n\t\t\tfsStats: map[string]*system.FsStats{\n\t\t\t\t\"sda\": {Root: true, Mountpoint: \"/\"},\n\t\t\t\t\"sdb\": {Root: false, Mountpoint: \"/mnt/storage\"},\n\t\t\t},\n\t\t\tdiskUsageCacheDuration: 1 * time.Hour,\n\t\t\t// lastDiskUsageUpdate is zero (first call)\n\t\t}\n\n\t\tvar stats system.Stats\n\t\tagent.updateDiskUsage(&stats)\n\n\t\t// After first call, lastDiskUsageUpdate should be set\n\t\tassert.False(t, agent.lastDiskUsageUpdate.IsZero(),\n\t\t\t\"lastDiskUsageUpdate should be set after first call\")\n\t})\n\n\tt.Run(\"expired cache updates extra filesystems\", func(t *testing.T) {\n\t\tagent := &Agent{\n\t\t\tfsStats: map[string]*system.FsStats{\n\t\t\t\t\"sda\": {Root: true, Mountpoint: \"/\"},\n\t\t\t\t\"sdb\": {Root: false, Mountpoint: \"/mnt/storage\"},\n\t\t\t},\n\t\t\tdiskUsageCacheDuration: 1 * time.Millisecond,\n\t\t\tlastDiskUsageUpdate:    time.Now().Add(-1 * time.Second), // cache expired\n\t\t}\n\n\t\tvar stats system.Stats\n\t\tagent.updateDiskUsage(&stats)\n\n\t\t// lastDiskUsageUpdate should be refreshed since cache expired\n\t\tassert.True(t, time.Since(agent.lastDiskUsageUpdate) < time.Second,\n\t\t\t\"lastDiskUsageUpdate should be refreshed when cache expires\")\n\t})\n}\n\nfunc TestHasSameDiskUsage(t *testing.T) {\n\tconst toleranceBytes uint64 = 16 * 1024 * 1024\n\n\tt.Run(\"returns true when totals and usage are equal\", func(t *testing.T) {\n\t\ta := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}\n\t\tb := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}\n\t\tassert.True(t, hasSameDiskUsage(a, b))\n\t})\n\n\tt.Run(\"returns true within tolerance\", func(t *testing.T) {\n\t\ta := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}\n\t\tb := &disk.UsageStat{\n\t\t\tTotal: a.Total + toleranceBytes - 1,\n\t\t\tUsed:  a.Used - toleranceBytes + 1,\n\t\t}\n\t\tassert.True(t, hasSameDiskUsage(a, b))\n\t})\n\n\tt.Run(\"returns false when total exceeds tolerance\", func(t *testing.T) {\n\t\ta := &disk.UsageStat{Total: 100 * 1024 * 1024 * 1024, Used: 42 * 1024 * 1024 * 1024}\n\t\tb := &disk.UsageStat{\n\t\t\tTotal: a.Total + toleranceBytes + 1,\n\t\t\tUsed:  a.Used,\n\t\t}\n\t\tassert.False(t, hasSameDiskUsage(a, b))\n\t})\n\n\tt.Run(\"returns false for nil or zero total\", func(t *testing.T) {\n\t\tassert.False(t, hasSameDiskUsage(nil, &disk.UsageStat{Total: 1, Used: 1}))\n\t\tassert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 1, Used: 1}, nil))\n\t\tassert.False(t, hasSameDiskUsage(&disk.UsageStat{Total: 0, Used: 0}, &disk.UsageStat{Total: 1, Used: 1}))\n\t})\n}\n\nfunc TestInitializeDiskIoStatsResetsTrackedDevices(t *testing.T) {\n\tagent := &Agent{\n\t\tfsStats: map[string]*system.FsStats{\n\t\t\t\"sda\": {},\n\t\t\t\"sdb\": {},\n\t\t},\n\t\tfsNames: []string{\"stale\", \"sda\"},\n\t}\n\n\tagent.initializeDiskIoStats(map[string]disk.IOCountersStat{\n\t\t\"sda\": {Name: \"sda\", ReadBytes: 10, WriteBytes: 20},\n\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 30, WriteBytes: 40},\n\t})\n\n\tassert.ElementsMatch(t, []string{\"sda\", \"sdb\"}, agent.fsNames)\n\tassert.Len(t, agent.fsNames, 2)\n\tassert.Equal(t, uint64(10), agent.fsStats[\"sda\"].TotalRead)\n\tassert.Equal(t, uint64(20), agent.fsStats[\"sda\"].TotalWrite)\n\tassert.False(t, agent.fsStats[\"sda\"].Time.IsZero())\n\tassert.False(t, agent.fsStats[\"sdb\"].Time.IsZero())\n\n\tagent.initializeDiskIoStats(map[string]disk.IOCountersStat{\n\t\t\"sdb\": {Name: \"sdb\", ReadBytes: 50, WriteBytes: 60},\n\t})\n\n\tassert.Equal(t, []string{\"sdb\"}, agent.fsNames)\n\tassert.Equal(t, uint64(50), agent.fsStats[\"sdb\"].TotalRead)\n\tassert.Equal(t, uint64(60), agent.fsStats[\"sdb\"].TotalWrite)\n}\n"
  },
  {
    "path": "agent/docker.go",
    "content": "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/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/deltatracker\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\n\t\"github.com/blang/semver\"\n)\n\n// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)\n// This includes CSI sequences like \\x1b[...m and simple escapes like \\x1b[K\nvar ansiEscapePattern = regexp.MustCompile(`\\x1b\\[[0-9;]*[a-zA-Z]|\\x1b\\][^\\x07]*\\x07|\\x1b[@-Z\\\\-_]`)\nvar dockerContainerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)\n\nconst (\n\t// Docker API timeout in milliseconds\n\tdockerTimeoutMs = 2100\n\t// Maximum realistic network speed (5 GB/s) to detect bad deltas\n\tmaxNetworkSpeedBps uint64 = 5e9\n\t// Maximum conceivable memory usage of a container (100TB) to detect bad memory stats\n\tmaxMemoryUsage uint64 = 100 * 1024 * 1024 * 1024 * 1024\n\t// Number of log lines to request when fetching container logs\n\tdockerLogsTail = 200\n\t// Maximum size of a single log frame (1MB) to prevent memory exhaustion\n\t// A single log line larger than 1MB is likely an error or misconfiguration\n\tmaxLogFrameSize = 1024 * 1024\n\t// Maximum total log content size (5MB) to prevent memory exhaustion\n\t// This provides a reasonable limit for network transfer and browser rendering\n\tmaxTotalLogSize = 5 * 1024 * 1024\n)\n\ntype dockerManager struct {\n\tclient              *http.Client                // Client to query Docker API\n\twg                  sync.WaitGroup              // WaitGroup to wait for all goroutines to finish\n\tsem                 chan struct{}               // Semaphore to limit concurrent container requests\n\tcontainerStatsMutex sync.RWMutex                // Mutex to prevent concurrent access to containerStatsMap\n\tapiContainerList    []*container.ApiInfo        // List of containers from Docker API\n\tcontainerStatsMap   map[string]*container.Stats // Keeps track of container stats\n\tvalidIds            map[string]struct{}         // Map of valid container ids, used to prune invalid containers from containerStatsMap\n\tgoodDockerVersion   bool                        // Whether docker version is at least 25.0.0 (one-shot works correctly)\n\tisWindows           bool                        // Whether the Docker Engine API is running on Windows\n\tbuf                 *bytes.Buffer               // Buffer to store and read response bodies\n\tdecoder             *json.Decoder               // Reusable JSON decoder that reads from buf\n\tapiStats            *container.ApiStats         // Reusable API stats object\n\texcludeContainers   []string                    // Patterns to exclude containers by name\n\tusingPodman         bool                        // Whether the Docker Engine API is running on Podman\n\n\t// Cache-time-aware tracking for CPU stats (similar to cpu.go)\n\t// Maps cache time intervals to container-specific CPU usage tracking\n\tlastCpuContainer map[uint16]map[string]uint64    // cacheTimeMs -> containerId -> last cpu container usage\n\tlastCpuSystem    map[uint16]map[string]uint64    // cacheTimeMs -> containerId -> last cpu system usage\n\tlastCpuReadTime  map[uint16]map[string]time.Time // cacheTimeMs -> containerId -> last read time (Windows)\n\n\t// Network delta trackers - one per cache time to avoid interference\n\t// cacheTimeMs -> DeltaTracker for network bytes sent/received\n\tnetworkSentTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]\n\tnetworkRecvTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64]\n\tretrySleep          func(time.Duration)\n}\n\n// userAgentRoundTripper is a custom http.RoundTripper that adds a User-Agent header to all requests\ntype userAgentRoundTripper struct {\n\trt        http.RoundTripper\n\tuserAgent string\n}\n\n// RoundTrip implements the http.RoundTripper interface\nfunc (u *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"User-Agent\", u.userAgent)\n\treturn u.rt.RoundTrip(req)\n}\n\n// Add goroutine to the queue\nfunc (d *dockerManager) queue() {\n\td.wg.Add(1)\n\tif d.goodDockerVersion {\n\t\td.sem <- struct{}{}\n\t}\n}\n\n// Remove goroutine from the queue\nfunc (d *dockerManager) dequeue() {\n\td.wg.Done()\n\tif d.goodDockerVersion {\n\t\t<-d.sem\n\t}\n}\n\n// shouldExcludeContainer checks if a container name matches any exclusion pattern\nfunc (dm *dockerManager) shouldExcludeContainer(name string) bool {\n\tif len(dm.excludeContainers) == 0 {\n\t\treturn false\n\t}\n\tfor _, pattern := range dm.excludeContainers {\n\t\tif match, _ := path.Match(pattern, name); match {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Returns stats for all running containers with cache-time-aware delta tracking\nfunc (dm *dockerManager) getDockerStats(cacheTimeMs uint16) ([]*container.Stats, error) {\n\tresp, err := dm.client.Get(\"http://localhost/containers/json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdm.apiContainerList = dm.apiContainerList[:0]\n\tif err := dm.decode(resp, &dm.apiContainerList); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdm.isWindows = strings.Contains(resp.Header.Get(\"Server\"), \"windows\")\n\n\tcontainersLength := len(dm.apiContainerList)\n\n\t// store valid ids to clean up old container ids from map\n\tif dm.validIds == nil {\n\t\tdm.validIds = make(map[string]struct{}, containersLength)\n\t} else {\n\t\tclear(dm.validIds)\n\t}\n\n\tvar failedContainers []*container.ApiInfo\n\n\tfor _, ctr := range dm.apiContainerList {\n\t\tctr.IdShort = ctr.Id[:12]\n\n\t\t// Skip this container if it matches the exclusion pattern\n\t\tif dm.shouldExcludeContainer(ctr.Names[0][1:]) {\n\t\t\tslog.Debug(\"Excluding container\", \"name\", ctr.Names[0][1:])\n\t\t\tcontinue\n\t\t}\n\n\t\tdm.validIds[ctr.IdShort] = struct{}{}\n\t\t// check if container is less than 1 minute old (possible restart)\n\t\t// note: can't use Created field because it's not updated on restart\n\t\tif strings.Contains(ctr.Status, \"second\") {\n\t\t\t// if so, remove old container data\n\t\t\tdm.deleteContainerStatsSync(ctr.IdShort)\n\t\t}\n\t\tdm.queue()\n\t\tgo func(ctr *container.ApiInfo) {\n\t\t\tdefer dm.dequeue()\n\t\t\terr := dm.updateContainerStats(ctr, cacheTimeMs)\n\t\t\t// if error, delete from map and add to failed list to retry\n\t\t\tif err != nil {\n\t\t\t\tdm.containerStatsMutex.Lock()\n\t\t\t\tdelete(dm.containerStatsMap, ctr.IdShort)\n\t\t\t\tfailedContainers = append(failedContainers, ctr)\n\t\t\t\tdm.containerStatsMutex.Unlock()\n\t\t\t}\n\t\t}(ctr)\n\t}\n\n\tdm.wg.Wait()\n\n\t// retry failed containers separately so we can run them in parallel (docker 24 bug)\n\tif len(failedContainers) > 0 {\n\t\tslog.Debug(\"Retrying failed containers\", \"count\", len(failedContainers))\n\t\tfor i := range failedContainers {\n\t\t\tctr := failedContainers[i]\n\t\t\tdm.queue()\n\t\t\tgo func(ctr *container.ApiInfo) {\n\t\t\t\tdefer dm.dequeue()\n\t\t\t\tif err2 := dm.updateContainerStats(ctr, cacheTimeMs); err2 != nil {\n\t\t\t\t\tslog.Error(\"Error getting container stats\", \"err\", err2)\n\t\t\t\t}\n\t\t\t}(ctr)\n\t\t}\n\t\tdm.wg.Wait()\n\t}\n\n\t// populate final stats and remove old / invalid container stats\n\tstats := make([]*container.Stats, 0, containersLength)\n\tfor id, v := range dm.containerStatsMap {\n\t\tif _, exists := dm.validIds[id]; !exists {\n\t\t\tdelete(dm.containerStatsMap, id)\n\t\t} else {\n\t\t\tstats = append(stats, v)\n\t\t}\n\t}\n\n\t// prepare network trackers for next interval for this cache time\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\n\treturn stats, nil\n}\n\n// initializeCpuTracking initializes CPU tracking maps for a specific cache time interval\nfunc (dm *dockerManager) initializeCpuTracking(cacheTimeMs uint16) {\n\t// Initialize cache time maps if they don't exist\n\tif dm.lastCpuContainer[cacheTimeMs] == nil {\n\t\tdm.lastCpuContainer[cacheTimeMs] = make(map[string]uint64)\n\t}\n\tif dm.lastCpuSystem[cacheTimeMs] == nil {\n\t\tdm.lastCpuSystem[cacheTimeMs] = make(map[string]uint64)\n\t}\n\t// Ensure the outer map exists before indexing\n\tif dm.lastCpuReadTime == nil {\n\t\tdm.lastCpuReadTime = make(map[uint16]map[string]time.Time)\n\t}\n\tif dm.lastCpuReadTime[cacheTimeMs] == nil {\n\t\tdm.lastCpuReadTime[cacheTimeMs] = make(map[string]time.Time)\n\t}\n}\n\n// getCpuPreviousValues returns previous CPU values for a container and cache time interval\nfunc (dm *dockerManager) getCpuPreviousValues(cacheTimeMs uint16, containerId string) (uint64, uint64) {\n\treturn dm.lastCpuContainer[cacheTimeMs][containerId], dm.lastCpuSystem[cacheTimeMs][containerId]\n}\n\n// setCpuCurrentValues stores current CPU values for a container and cache time interval\nfunc (dm *dockerManager) setCpuCurrentValues(cacheTimeMs uint16, containerId string, cpuContainer, cpuSystem uint64) {\n\tdm.lastCpuContainer[cacheTimeMs][containerId] = cpuContainer\n\tdm.lastCpuSystem[cacheTimeMs][containerId] = cpuSystem\n}\n\n// calculateMemoryUsage calculates memory usage from Docker API stats\nfunc calculateMemoryUsage(apiStats *container.ApiStats, isWindows bool) (uint64, error) {\n\tif isWindows {\n\t\treturn apiStats.MemoryStats.PrivateWorkingSet, nil\n\t}\n\n\tmemCache := apiStats.MemoryStats.Stats.InactiveFile\n\tif memCache == 0 {\n\t\tmemCache = apiStats.MemoryStats.Stats.Cache\n\t}\n\n\tusedDelta := apiStats.MemoryStats.Usage - memCache\n\tif usedDelta <= 0 || usedDelta > maxMemoryUsage {\n\t\treturn 0, fmt.Errorf(\"bad memory stats\")\n\t}\n\n\treturn usedDelta, nil\n}\n\n// getNetworkTracker returns the DeltaTracker for a specific cache time, creating it if needed\nfunc (dm *dockerManager) getNetworkTracker(cacheTimeMs uint16, isSent bool) *deltatracker.DeltaTracker[string, uint64] {\n\tvar trackers map[uint16]*deltatracker.DeltaTracker[string, uint64]\n\tif isSent {\n\t\ttrackers = dm.networkSentTrackers\n\t} else {\n\t\ttrackers = dm.networkRecvTrackers\n\t}\n\n\tif trackers[cacheTimeMs] == nil {\n\t\ttrackers[cacheTimeMs] = deltatracker.NewDeltaTracker[string, uint64]()\n\t}\n\n\treturn trackers[cacheTimeMs]\n}\n\n// cycleNetworkDeltasForCacheTime cycles the network delta trackers for a specific cache time\nfunc (dm *dockerManager) cycleNetworkDeltasForCacheTime(cacheTimeMs uint16) {\n\tif dm.networkSentTrackers[cacheTimeMs] != nil {\n\t\tdm.networkSentTrackers[cacheTimeMs].Cycle()\n\t}\n\tif dm.networkRecvTrackers[cacheTimeMs] != nil {\n\t\tdm.networkRecvTrackers[cacheTimeMs].Cycle()\n\t}\n}\n\n// calculateNetworkStats calculates network sent/receive deltas using DeltaTracker\nfunc (dm *dockerManager) calculateNetworkStats(ctr *container.ApiInfo, apiStats *container.ApiStats, stats *container.Stats, initialized bool, name string, cacheTimeMs uint16) (uint64, uint64) {\n\tvar total_sent, total_recv uint64\n\tfor _, v := range apiStats.Networks {\n\t\ttotal_sent += v.TxBytes\n\t\ttotal_recv += v.RxBytes\n\t}\n\n\t// Get the DeltaTracker for this specific cache time\n\tsentTracker := dm.getNetworkTracker(cacheTimeMs, true)\n\trecvTracker := dm.getNetworkTracker(cacheTimeMs, false)\n\n\t// Set current values in the cache-time-specific DeltaTracker\n\tsentTracker.Set(ctr.IdShort, total_sent)\n\trecvTracker.Set(ctr.IdShort, total_recv)\n\n\t// Get deltas (bytes since last measurement)\n\tsent_delta_raw := sentTracker.Delta(ctr.IdShort)\n\trecv_delta_raw := recvTracker.Delta(ctr.IdShort)\n\n\t// Calculate bytes per second independently for Tx and Rx if we have previous data\n\tvar sent_delta, recv_delta uint64\n\tif initialized {\n\t\tmillisecondsElapsed := uint64(time.Since(stats.PrevReadTime).Milliseconds())\n\t\tif millisecondsElapsed > 0 {\n\t\t\tif sent_delta_raw > 0 {\n\t\t\t\tsent_delta = sent_delta_raw * 1000 / millisecondsElapsed\n\t\t\t\tif sent_delta > maxNetworkSpeedBps {\n\t\t\t\t\tslog.Warn(\"Bad network delta\", \"container\", name)\n\t\t\t\t\tsent_delta = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\tif recv_delta_raw > 0 {\n\t\t\t\trecv_delta = recv_delta_raw * 1000 / millisecondsElapsed\n\t\t\t\tif recv_delta > maxNetworkSpeedBps {\n\t\t\t\t\tslog.Warn(\"Bad network delta\", \"container\", name)\n\t\t\t\t\trecv_delta = 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sent_delta, recv_delta\n}\n\n// validateCpuPercentage checks if CPU percentage is within valid range\nfunc validateCpuPercentage(cpuPct float64, containerName string) error {\n\tif cpuPct > 100 {\n\t\treturn fmt.Errorf(\"%s cpu pct greater than 100: %+v\", containerName, cpuPct)\n\t}\n\treturn nil\n}\n\n// updateContainerStatsValues updates the final stats values\nfunc updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemory uint64, sent_delta, recv_delta uint64, readTime time.Time) {\n\tstats.Cpu = utils.TwoDecimals(cpuPct)\n\tstats.Mem = utils.BytesToMegabytes(float64(usedMemory))\n\tstats.Bandwidth = [2]uint64{sent_delta, recv_delta}\n\t// TODO(0.19+): stop populating NetworkSent/NetworkRecv (deprecated in 0.18.3)\n\tstats.NetworkSent = utils.BytesToMegabytes(float64(sent_delta))\n\tstats.NetworkRecv = utils.BytesToMegabytes(float64(recv_delta))\n\tstats.PrevReadTime = readTime\n}\n\n// convertContainerPortsToString formats the ports of a container into a sorted, deduplicated string.\n// ctr.Ports is nilled out after processing so the slice is not accidentally reused.\nfunc convertContainerPortsToString(ctr *container.ApiInfo) string {\n\tif len(ctr.Ports) == 0 {\n\t\treturn \"\"\n\t}\n\tsort.Slice(ctr.Ports, func(i, j int) bool {\n\t\treturn ctr.Ports[i].PublicPort < ctr.Ports[j].PublicPort\n\t})\n\tvar builder strings.Builder\n\tseenPorts := make(map[uint16]struct{})\n\tfor _, p := range ctr.Ports {\n\t\t_, ok := seenPorts[p.PublicPort]\n\t\tif p.PublicPort == 0 || ok {\n\t\t\tcontinue\n\t\t}\n\t\tseenPorts[p.PublicPort] = struct{}{}\n\t\tif builder.Len() > 0 {\n\t\t\tbuilder.WriteString(\", \")\n\t\t}\n\t\tswitch p.IP {\n\t\tcase \"0.0.0.0\", \"::\":\n\t\tdefault:\n\t\t\tbuilder.WriteString(p.IP)\n\t\t\tbuilder.WriteByte(':')\n\t\t}\n\t\tbuilder.WriteString(strconv.Itoa(int(p.PublicPort)))\n\t}\n\t// clear ports slice so it doesn't get reused and blend into next response\n\tctr.Ports = nil\n\treturn builder.String()\n}\n\nfunc parseDockerStatus(status string) (string, container.DockerHealth) {\n\ttrimmed := strings.TrimSpace(status)\n\tif trimmed == \"\" {\n\t\treturn \"\", container.DockerHealthNone\n\t}\n\n\t// Remove \"About \" from status\n\ttrimmed = strings.Replace(trimmed, \"About \", \"\", 1)\n\n\topenIdx := strings.LastIndex(trimmed, \"(\")\n\tif openIdx == -1 || !strings.HasSuffix(trimmed, \")\") {\n\t\treturn trimmed, container.DockerHealthNone\n\t}\n\n\tstatusText := strings.TrimSpace(trimmed[:openIdx])\n\tif statusText == \"\" {\n\t\tstatusText = trimmed\n\t}\n\n\thealthText := strings.TrimSpace(strings.TrimSuffix(trimmed[openIdx+1:], \")\"))\n\t// Some Docker statuses include a \"health:\" prefix inside the parentheses.\n\t// Strip it so it maps correctly to the known health states.\n\tif colonIdx := strings.IndexRune(healthText, ':'); colonIdx != -1 {\n\t\tprefix := strings.ToLower(strings.TrimSpace(healthText[:colonIdx]))\n\t\tif prefix == \"health\" || prefix == \"health status\" {\n\t\t\thealthText = strings.TrimSpace(healthText[colonIdx+1:])\n\t\t}\n\t}\n\tif health, ok := parseDockerHealthStatus(healthText); ok {\n\t\treturn statusText, health\n\t}\n\n\treturn trimmed, container.DockerHealthNone\n}\n\n// parseDockerHealthStatus maps Docker health status strings to container.DockerHealth values\nfunc parseDockerHealthStatus(status string) (container.DockerHealth, bool) {\n\thealth, ok := container.DockerHealthStrings[strings.ToLower(strings.TrimSpace(status))]\n\treturn health, ok\n}\n\n// getPodmanContainerHealth fetches container health status from the container inspect endpoint.\n// Used for Podman which doesn't provide health status in the /containers/json endpoint as of March 2026.\n// https://github.com/containers/podman/issues/27786\nfunc (dm *dockerManager) getPodmanContainerHealth(containerID string) (container.DockerHealth, error) {\n\tresp, err := dm.client.Get(fmt.Sprintf(\"http://localhost/containers/%s/json\", url.PathEscape(containerID)))\n\tif err != nil {\n\t\treturn container.DockerHealthNone, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn container.DockerHealthNone, fmt.Errorf(\"container inspect request failed: %s\", resp.Status)\n\t}\n\n\tvar inspectInfo struct {\n\t\tState struct {\n\t\t\tHealth struct {\n\t\t\t\tStatus string\n\t\t\t}\n\t\t}\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&inspectInfo); err != nil {\n\t\treturn container.DockerHealthNone, err\n\t}\n\n\tif health, ok := parseDockerHealthStatus(inspectInfo.State.Health.Status); ok {\n\t\treturn health, nil\n\t}\n\n\treturn container.DockerHealthNone, nil\n}\n\n// Updates stats for individual container with cache-time-aware delta tracking\nfunc (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeMs uint16) error {\n\tname := ctr.Names[0][1:]\n\n\tresp, err := dm.client.Get(fmt.Sprintf(\"http://localhost/containers/%s/stats?stream=0&one-shot=1\", ctr.IdShort))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstatusText, health := parseDockerStatus(ctr.Status)\n\n\t// Docker exposes Health.Status on /containers/json in API 1.52+.\n\t// Podman currently requires falling back to the inspect endpoint as of March 2026.\n\t// https://github.com/containers/podman/issues/27786\n\tif ctr.Health.Status != \"\" {\n\t\tif h, ok := parseDockerHealthStatus(ctr.Health.Status); ok {\n\t\t\thealth = h\n\t\t}\n\t} else if dm.usingPodman {\n\t\tif podmanHealth, err := dm.getPodmanContainerHealth(ctr.IdShort); err == nil {\n\t\t\thealth = podmanHealth\n\t\t}\n\t}\n\n\tdm.containerStatsMutex.Lock()\n\tdefer dm.containerStatsMutex.Unlock()\n\n\t// add empty values if they doesn't exist in map\n\tstats, initialized := dm.containerStatsMap[ctr.IdShort]\n\tif !initialized {\n\t\tstats = &container.Stats{Name: name, Id: ctr.IdShort, Image: ctr.Image}\n\t\tdm.containerStatsMap[ctr.IdShort] = stats\n\t}\n\n\tstats.Id = ctr.IdShort\n\tstats.Status = statusText\n\tstats.Health = health\n\n\tif len(ctr.Ports) > 0 {\n\t\tstats.Ports = convertContainerPortsToString(ctr)\n\t}\n\n\t// reset current stats\n\tstats.Cpu = 0\n\tstats.Mem = 0\n\tstats.Bandwidth = [2]uint64{0, 0}\n\t// TODO(0.19+): stop populating NetworkSent/NetworkRecv (deprecated in 0.18.3)\n\tstats.NetworkSent = 0\n\tstats.NetworkRecv = 0\n\n\tres := dm.apiStats\n\tres.Networks = nil\n\tif err := dm.decode(resp, res); err != nil {\n\t\treturn err\n\t}\n\n\t// Initialize CPU tracking for this cache time interval\n\tdm.initializeCpuTracking(cacheTimeMs)\n\n\t// Get previous CPU values\n\tprevCpuContainer, prevCpuSystem := dm.getCpuPreviousValues(cacheTimeMs, ctr.IdShort)\n\n\t// Calculate CPU percentage based on platform\n\tvar cpuPct float64\n\tif dm.isWindows {\n\t\tprevRead := dm.lastCpuReadTime[cacheTimeMs][ctr.IdShort]\n\t\tcpuPct = res.CalculateCpuPercentWindows(prevCpuContainer, prevRead)\n\t} else {\n\t\tcpuPct = res.CalculateCpuPercentLinux(prevCpuContainer, prevCpuSystem)\n\t}\n\n\t// Calculate memory usage\n\tusedMemory, err := calculateMemoryUsage(res, dm.isWindows)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s - %w - see https://github.com/henrygd/beszel/issues/144\", name, err)\n\t}\n\n\t// Store current CPU stats for next calculation\n\tcurrentCpuContainer := res.CPUStats.CPUUsage.TotalUsage\n\tcurrentCpuSystem := res.CPUStats.SystemUsage\n\tdm.setCpuCurrentValues(cacheTimeMs, ctr.IdShort, currentCpuContainer, currentCpuSystem)\n\n\t// Validate CPU percentage\n\tif err := validateCpuPercentage(cpuPct, name); err != nil {\n\t\treturn err\n\t}\n\n\t// Calculate network stats using DeltaTracker\n\tsent_delta, recv_delta := dm.calculateNetworkStats(ctr, res, stats, initialized, name, cacheTimeMs)\n\n\t// Store current network values for legacy compatibility\n\tvar total_sent, total_recv uint64\n\tfor _, v := range res.Networks {\n\t\ttotal_sent += v.TxBytes\n\t\ttotal_recv += v.RxBytes\n\t}\n\tstats.PrevNet.Sent, stats.PrevNet.Recv = total_sent, total_recv\n\n\t// Update final stats values\n\tupdateContainerStatsValues(stats, cpuPct, usedMemory, sent_delta, recv_delta, res.Read)\n\t// store per-cache-time read time for Windows CPU percent calc\n\tdm.lastCpuReadTime[cacheTimeMs][ctr.IdShort] = res.Read\n\n\treturn nil\n}\n\n// Delete container stats from map using mutex\nfunc (dm *dockerManager) deleteContainerStatsSync(id string) {\n\tdm.containerStatsMutex.Lock()\n\tdefer dm.containerStatsMutex.Unlock()\n\tdelete(dm.containerStatsMap, id)\n\tfor ct := range dm.lastCpuContainer {\n\t\tdelete(dm.lastCpuContainer[ct], id)\n\t}\n\tfor ct := range dm.lastCpuSystem {\n\t\tdelete(dm.lastCpuSystem[ct], id)\n\t}\n\tfor ct := range dm.lastCpuReadTime {\n\t\tdelete(dm.lastCpuReadTime[ct], id)\n\t}\n}\n\n// Creates a new http client for Docker or Podman API\nfunc newDockerManager() *dockerManager {\n\tdockerHost, exists := utils.GetEnv(\"DOCKER_HOST\")\n\tif exists {\n\t\t// return nil if set to empty string\n\t\tif dockerHost == \"\" {\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\tdockerHost = getDockerHost()\n\t}\n\n\tparsedURL, err := url.Parse(dockerHost)\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n\n\ttransport := &http.Transport{\n\t\tDisableCompression: true,\n\t\tMaxConnsPerHost:    0,\n\t}\n\n\tswitch parsedURL.Scheme {\n\tcase \"unix\":\n\t\ttransport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {\n\t\t\treturn (&net.Dialer{}).DialContext(ctx, \"unix\", parsedURL.Path)\n\t\t}\n\tcase \"tcp\", \"http\", \"https\":\n\t\ttransport.DialContext = func(ctx context.Context, proto, addr string) (net.Conn, error) {\n\t\t\treturn (&net.Dialer{}).DialContext(ctx, \"tcp\", parsedURL.Host)\n\t\t}\n\tdefault:\n\t\tslog.Error(\"Invalid DOCKER_HOST\", \"scheme\", parsedURL.Scheme)\n\t\tos.Exit(1)\n\t}\n\n\t// configurable timeout\n\ttimeout := time.Millisecond * time.Duration(dockerTimeoutMs)\n\tif t, set := utils.GetEnv(\"DOCKER_TIMEOUT\"); set {\n\t\ttimeout, err = time.ParseDuration(t)\n\t\tif err != nil {\n\t\t\tslog.Error(err.Error())\n\t\t\tos.Exit(1)\n\t\t}\n\t\tslog.Info(\"DOCKER_TIMEOUT\", \"timeout\", timeout)\n\t}\n\n\t// Custom user-agent to avoid docker bug: https://github.com/docker/for-mac/issues/7575\n\tuserAgentTransport := &userAgentRoundTripper{\n\t\trt:        transport,\n\t\tuserAgent: \"Docker-Client/\",\n\t}\n\n\t// Read container exclusion patterns from environment variable\n\tvar excludeContainers []string\n\tif excludeStr, set := utils.GetEnv(\"EXCLUDE_CONTAINERS\"); set && excludeStr != \"\" {\n\t\tparts := strings.SplitSeq(excludeStr, \",\")\n\t\tfor part := range parts {\n\t\t\ttrimmed := strings.TrimSpace(part)\n\t\t\tif trimmed != \"\" {\n\t\t\t\texcludeContainers = append(excludeContainers, trimmed)\n\t\t\t}\n\t\t}\n\t\tslog.Info(\"EXCLUDE_CONTAINERS\", \"patterns\", excludeContainers)\n\t}\n\n\tmanager := &dockerManager{\n\t\tclient: &http.Client{\n\t\t\tTimeout:   timeout,\n\t\t\tTransport: userAgentTransport,\n\t\t},\n\t\tcontainerStatsMap: make(map[string]*container.Stats),\n\t\tsem:               make(chan struct{}, 5),\n\t\tapiContainerList:  []*container.ApiInfo{},\n\t\tapiStats:          &container.ApiStats{},\n\t\texcludeContainers: excludeContainers,\n\n\t\t// Initialize cache-time-aware tracking structures\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:     make(map[uint16]map[string]time.Time),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tretrySleep:          time.Sleep,\n\t}\n\n\t// If using podman, return client\n\tif strings.Contains(dockerHost, \"podman\") {\n\t\tmanager.usingPodman = true\n\t\tmanager.goodDockerVersion = true\n\t\treturn manager\n\t}\n\n\t// run version check in goroutine to avoid blocking (server may not be ready and requires retries)\n\tgo manager.checkDockerVersion()\n\n\t// give version check a chance to complete before returning\n\ttime.Sleep(50 * time.Millisecond)\n\n\treturn manager\n}\n\n// checkDockerVersion checks Docker version and sets goodDockerVersion if at least 25.0.0.\n// Versions before 25.0.0 have a bug with one-shot which requires all requests to be made in one batch.\nfunc (dm *dockerManager) checkDockerVersion() {\n\tvar err error\n\tvar resp *http.Response\n\tvar versionInfo struct {\n\t\tVersion string `json:\"Version\"`\n\t}\n\tconst versionMaxTries = 2\n\tfor i := 1; i <= versionMaxTries; i++ {\n\t\tresp, err = dm.client.Get(\"http://localhost/version\")\n\t\tif err == nil && resp.StatusCode == http.StatusOK {\n\t\t\tbreak\n\t\t}\n\t\tif resp != nil {\n\t\t\tresp.Body.Close()\n\t\t}\n\t\tif i < versionMaxTries {\n\t\t\tslog.Debug(\"Failed to get Docker version; retrying\", \"attempt\", i, \"err\", err, \"response\", resp)\n\t\t\tdm.retrySleep(5 * time.Second)\n\t\t}\n\t}\n\tif err != nil || resp.StatusCode != http.StatusOK {\n\t\treturn\n\t}\n\tif err := dm.decode(resp, &versionInfo); err != nil {\n\t\treturn\n\t}\n\t// if version > 24, one-shot works correctly and we can limit concurrent operations\n\tif dockerVersion, err := semver.Parse(versionInfo.Version); err == nil && dockerVersion.Major > 24 {\n\t\tdm.goodDockerVersion = true\n\t} else {\n\t\tslog.Info(fmt.Sprintf(\"Docker %s is outdated. Upgrade if possible. See https://github.com/henrygd/beszel/issues/58\", versionInfo.Version))\n\t}\n}\n\n// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.\nfunc (dm *dockerManager) decode(resp *http.Response, d any) error {\n\tif dm.buf == nil {\n\t\t// initialize buffer with 256kb starting size\n\t\tdm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256))\n\t\tdm.decoder = json.NewDecoder(dm.buf)\n\t}\n\tdefer resp.Body.Close()\n\tdefer dm.buf.Reset()\n\t_, err := dm.buf.ReadFrom(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn dm.decoder.Decode(d)\n}\n\n// Test docker / podman sockets and return if one exists\nfunc getDockerHost() string {\n\tscheme := \"unix://\"\n\tsocks := []string{\"/var/run/docker.sock\", fmt.Sprintf(\"/run/user/%v/podman/podman.sock\", os.Getuid())}\n\tfor _, sock := range socks {\n\t\tif _, err := os.Stat(sock); err == nil {\n\t\t\treturn scheme + sock\n\t\t}\n\t}\n\treturn scheme + socks[0]\n}\n\nfunc validateContainerID(containerID string) error {\n\tif !dockerContainerIDPattern.MatchString(containerID) {\n\t\treturn fmt.Errorf(\"invalid container id\")\n\t}\n\treturn nil\n}\n\nfunc buildDockerContainerEndpoint(containerID, action string, query url.Values) (string, error) {\n\tif err := validateContainerID(containerID); err != nil {\n\t\treturn \"\", err\n\t}\n\tu := &url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   \"localhost\",\n\t\tPath:   fmt.Sprintf(\"/containers/%s/%s\", url.PathEscape(containerID), action),\n\t}\n\tif len(query) > 0 {\n\t\tu.RawQuery = query.Encode()\n\t}\n\treturn u.String(), nil\n}\n\n// getContainerInfo fetches the inspection data for a container\nfunc (dm *dockerManager) getContainerInfo(ctx context.Context, containerID string) ([]byte, error) {\n\tendpoint, err := buildDockerContainerEndpoint(containerID, \"json\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := dm.client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))\n\t\treturn nil, fmt.Errorf(\"container info request failed: %s: %s\", resp.Status, strings.TrimSpace(string(body)))\n\t}\n\n\t// Remove sensitive environment variables from Config.Env\n\tvar containerInfo map[string]any\n\tif err := json.NewDecoder(resp.Body).Decode(&containerInfo); err != nil {\n\t\treturn nil, err\n\t}\n\tif config, ok := containerInfo[\"Config\"].(map[string]any); ok {\n\t\tdelete(config, \"Env\")\n\t}\n\n\treturn json.Marshal(containerInfo)\n}\n\n// getLogs fetches the logs for a container\nfunc (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {\n\tquery := url.Values{\n\t\t\"stdout\": []string{\"1\"},\n\t\t\"stderr\": []string{\"1\"},\n\t\t\"tail\":   []string{fmt.Sprintf(\"%d\", dockerLogsTail)},\n\t}\n\tendpoint, err := buildDockerContainerEndpoint(containerID, \"logs\", query)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := dm.client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))\n\t\treturn \"\", fmt.Errorf(\"logs request failed: %s: %s\", resp.Status, strings.TrimSpace(string(body)))\n\t}\n\n\tvar builder strings.Builder\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tmultiplexed := strings.HasSuffix(contentType, \"multiplexed-stream\")\n\tlogReader := io.Reader(resp.Body)\n\tif !multiplexed {\n\t\t// Podman may return multiplexed logs without Content-Type. Sniff the first frame header\n\t\t// with a small buffered reader only when the header check fails.\n\t\tbufferedReader := bufio.NewReaderSize(resp.Body, 8)\n\t\tmultiplexed = detectDockerMultiplexedStream(bufferedReader)\n\t\tlogReader = bufferedReader\n\t}\n\tif err := decodeDockerLogStream(logReader, &builder, multiplexed); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Strip ANSI escape sequences from logs for clean display in web UI\n\tlogs := builder.String()\n\tif strings.Contains(logs, \"\\x1b\") {\n\t\tlogs = ansiEscapePattern.ReplaceAllString(logs, \"\")\n\t}\n\treturn logs, nil\n}\n\nfunc detectDockerMultiplexedStream(reader *bufio.Reader) bool {\n\tconst headerSize = 8\n\theader, err := reader.Peek(headerSize)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif header[0] != 0x01 && header[0] != 0x02 {\n\t\treturn false\n\t}\n\t// Docker's stream framing header reserves bytes 1-3 as zero.\n\tif header[1] != 0 || header[2] != 0 || header[3] != 0 {\n\t\treturn false\n\t}\n\tframeLen := binary.BigEndian.Uint32(header[4:])\n\treturn frameLen <= maxLogFrameSize\n}\n\nfunc decodeDockerLogStream(reader io.Reader, builder *strings.Builder, multiplexed bool) error {\n\tif !multiplexed {\n\t\t_, err := io.Copy(builder, io.LimitReader(reader, maxTotalLogSize))\n\t\treturn err\n\t}\n\tconst headerSize = 8\n\tvar header [headerSize]byte\n\ttotalBytesRead := 0\n\n\tfor {\n\t\tif _, err := io.ReadFull(reader, header[:]); err != nil {\n\t\t\tif errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tframeLen := binary.BigEndian.Uint32(header[4:])\n\t\tif frameLen == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Prevent memory exhaustion from excessively large frames\n\t\tif frameLen > maxLogFrameSize {\n\t\t\treturn fmt.Errorf(\"log frame size (%d) exceeds maximum (%d)\", frameLen, maxLogFrameSize)\n\t\t}\n\n\t\t// Check if reading this frame would exceed total log size limit\n\t\tif totalBytesRead+int(frameLen) > maxTotalLogSize {\n\t\t\t// Read and discard remaining data to avoid blocking\n\t\t\t_, _ = io.CopyN(io.Discard, reader, int64(frameLen))\n\t\t\tslog.Debug(\"Truncating logs: limit reached\", \"read\", totalBytesRead, \"limit\", maxTotalLogSize)\n\t\t\treturn nil\n\t\t}\n\n\t\tn, err := io.CopyN(builder, reader, int64(frameLen))\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\ttotalBytesRead += int(n)\n\t}\n}\n\n// GetHostInfo fetches the system info from Docker\nfunc (dm *dockerManager) GetHostInfo() (info container.HostInfo, err error) {\n\tresp, err := dm.client.Get(\"http://localhost/info\")\n\tif err != nil {\n\t\treturn info, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif err := json.NewDecoder(resp.Body).Decode(&info); err != nil {\n\t\treturn info, err\n\t}\n\n\treturn info, nil\n}\n\nfunc (dm *dockerManager) IsPodman() bool {\n\treturn dm.usingPodman\n}\n"
  },
  {
    "path": "agent/docker_test.go",
    "content": "//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/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/deltatracker\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar defaultCacheTimeMs = uint16(60_000)\n\ntype recordingRoundTripper struct {\n\tstatusCode  int\n\tbody        string\n\tcontentType string\n\tcalled      bool\n\tlastPath    string\n\tlastQuery   map[string]string\n}\n\ntype roundTripFunc func(*http.Request) (*http.Response, error)\n\nfunc (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn fn(req)\n}\n\nfunc (rt *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {\n\trt.called = true\n\trt.lastPath = req.URL.EscapedPath()\n\trt.lastQuery = map[string]string{}\n\tfor key, values := range req.URL.Query() {\n\t\tif len(values) > 0 {\n\t\t\trt.lastQuery[key] = values[0]\n\t\t}\n\t}\n\tresp := &http.Response{\n\t\tStatusCode: rt.statusCode,\n\t\tStatus:     \"200 OK\",\n\t\tHeader:     make(http.Header),\n\t\tBody:       io.NopCloser(strings.NewReader(rt.body)),\n\t\tRequest:    req,\n\t}\n\tif rt.contentType != \"\" {\n\t\tresp.Header.Set(\"Content-Type\", rt.contentType)\n\t}\n\treturn resp, nil\n}\n\n// cycleCpuDeltas cycles the CPU tracking data for a specific cache time interval\nfunc (dm *dockerManager) cycleCpuDeltas(cacheTimeMs uint16) {\n\t// Clear the CPU tracking maps for this cache time interval\n\tif dm.lastCpuContainer[cacheTimeMs] != nil {\n\t\tclear(dm.lastCpuContainer[cacheTimeMs])\n\t}\n\tif dm.lastCpuSystem[cacheTimeMs] != nil {\n\t\tclear(dm.lastCpuSystem[cacheTimeMs])\n\t}\n}\n\nfunc TestCalculateMemoryUsage(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tapiStats    *container.ApiStats\n\t\tisWindows   bool\n\t\texpected    uint64\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"Linux with valid memory stats\",\n\t\t\tapiStats: &container.ApiStats{\n\t\t\t\tMemoryStats: container.MemoryStats{\n\t\t\t\t\tUsage: 1048576, // 1MB\n\t\t\t\t\tStats: container.MemoryStatsStats{\n\t\t\t\t\t\tCache:        524288, // 512KB\n\t\t\t\t\t\tInactiveFile: 262144, // 256KB\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisWindows:   false,\n\t\t\texpected:    786432, // 1MB - 256KB (inactive_file takes precedence) = 768KB\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Linux with zero cache uses inactive_file\",\n\t\t\tapiStats: &container.ApiStats{\n\t\t\t\tMemoryStats: container.MemoryStats{\n\t\t\t\t\tUsage: 1048576, // 1MB\n\t\t\t\t\tStats: container.MemoryStatsStats{\n\t\t\t\t\t\tCache:        0,\n\t\t\t\t\t\tInactiveFile: 262144, // 256KB\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisWindows:   false,\n\t\t\texpected:    786432, // 1MB - 256KB = 768KB\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Windows with valid memory stats\",\n\t\t\tapiStats: &container.ApiStats{\n\t\t\t\tMemoryStats: container.MemoryStats{\n\t\t\t\t\tPrivateWorkingSet: 524288, // 512KB\n\t\t\t\t},\n\t\t\t},\n\t\t\tisWindows:   true,\n\t\t\texpected:    524288,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Linux with zero usage returns error\",\n\t\t\tapiStats: &container.ApiStats{\n\t\t\t\tMemoryStats: container.MemoryStats{\n\t\t\t\t\tUsage: 0,\n\t\t\t\t\tStats: container.MemoryStatsStats{\n\t\t\t\t\t\tCache:        0,\n\t\t\t\t\t\tInactiveFile: 0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisWindows:   false,\n\t\t\texpected:    0,\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := calculateMemoryUsage(tt.apiStats, tt.isWindows)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildDockerContainerEndpoint(t *testing.T) {\n\tt.Run(\"valid container ID builds escaped endpoint\", func(t *testing.T) {\n\t\tendpoint, err := buildDockerContainerEndpoint(\"0123456789ab\", \"json\", nil)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"http://localhost/containers/0123456789ab/json\", endpoint)\n\t})\n\n\tt.Run(\"invalid container ID is rejected\", func(t *testing.T) {\n\t\t_, err := buildDockerContainerEndpoint(\"../../version\", \"json\", nil)\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"invalid container id\")\n\t})\n}\n\nfunc TestContainerDetailsRequestsValidateContainerID(t *testing.T) {\n\trt := &recordingRoundTripper{\n\t\tstatusCode: 200,\n\t\tbody:       `{\"Config\":{\"Env\":[\"SECRET=1\"]}}`,\n\t}\n\tdm := &dockerManager{\n\t\tclient: &http.Client{Transport: rt},\n\t}\n\n\t_, err := dm.getContainerInfo(context.Background(), \"../version\")\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid container id\")\n\tassert.False(t, rt.called, \"request should be rejected before dispatching to Docker API\")\n}\n\nfunc TestContainerDetailsRequestsUseExpectedDockerPaths(t *testing.T) {\n\tt.Run(\"container info uses container json endpoint\", func(t *testing.T) {\n\t\trt := &recordingRoundTripper{\n\t\t\tstatusCode: 200,\n\t\t\tbody:       `{\"Config\":{\"Env\":[\"SECRET=1\"]},\"Name\":\"demo\"}`,\n\t\t}\n\t\tdm := &dockerManager{\n\t\t\tclient: &http.Client{Transport: rt},\n\t\t}\n\n\t\tbody, err := dm.getContainerInfo(context.Background(), \"0123456789ab\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, rt.called)\n\t\tassert.Equal(t, \"/containers/0123456789ab/json\", rt.lastPath)\n\t\tassert.NotContains(t, string(body), \"SECRET=1\", \"sensitive env vars should be removed\")\n\t})\n\n\tt.Run(\"container logs uses expected endpoint and query params\", func(t *testing.T) {\n\t\trt := &recordingRoundTripper{\n\t\t\tstatusCode: 200,\n\t\t\tbody:       \"line1\\nline2\\n\",\n\t\t}\n\t\tdm := &dockerManager{\n\t\t\tclient: &http.Client{Transport: rt},\n\t\t}\n\n\t\tlogs, err := dm.getLogs(context.Background(), \"abcdef123456\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, rt.called)\n\t\tassert.Equal(t, \"/containers/abcdef123456/logs\", rt.lastPath)\n\t\tassert.Equal(t, \"1\", rt.lastQuery[\"stdout\"])\n\t\tassert.Equal(t, \"1\", rt.lastQuery[\"stderr\"])\n\t\tassert.Equal(t, \"200\", rt.lastQuery[\"tail\"])\n\t\tassert.Equal(t, \"line1\\nline2\\n\", logs)\n\t})\n}\n\nfunc TestGetPodmanContainerHealth(t *testing.T) {\n\tcalled := false\n\tdm := &dockerManager{\n\t\tclient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {\n\t\t\tcalled = true\n\t\t\tassert.Equal(t, \"/containers/0123456789ab/json\", req.URL.EscapedPath())\n\t\t\treturn &http.Response{\n\t\t\t\tStatusCode: http.StatusOK,\n\t\t\t\tStatus:     \"200 OK\",\n\t\t\t\tHeader:     make(http.Header),\n\t\t\t\tBody:       io.NopCloser(strings.NewReader(`{\"State\":{\"Health\":{\"Status\":\"healthy\"}}}`)),\n\t\t\t\tRequest:    req,\n\t\t\t}, nil\n\t\t})},\n\t}\n\n\thealth, err := dm.getPodmanContainerHealth(\"0123456789ab\")\n\trequire.NoError(t, err)\n\tassert.True(t, called)\n\tassert.Equal(t, container.DockerHealthHealthy, health)\n}\n\nfunc TestValidateCpuPercentage(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tcpuPct        float64\n\t\tcontainerName string\n\t\texpectError   bool\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tname:          \"valid CPU percentage\",\n\t\t\tcpuPct:        50.5,\n\t\t\tcontainerName: \"test-container\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"zero CPU percentage\",\n\t\t\tcpuPct:        0.0,\n\t\t\tcontainerName: \"test-container\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"CPU percentage over 100\",\n\t\t\tcpuPct:        150.5,\n\t\t\tcontainerName: \"test-container\",\n\t\t\texpectError:   true,\n\t\t\texpectedError: \"test-container cpu pct greater than 100: 150.5\",\n\t\t},\n\t\t{\n\t\t\tname:          \"CPU percentage exactly 100\",\n\t\t\tcpuPct:        100.0,\n\t\t\tcontainerName: \"test-container\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"negative CPU percentage\",\n\t\t\tcpuPct:        -10.0,\n\t\t\tcontainerName: \"test-container\",\n\t\t\texpectError:   false, // Function only checks for > 100, not negative\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateCpuPercentage(tt.cpuPct, tt.containerName)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.expectedError)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdateContainerStatsValues(t *testing.T) {\n\tstats := &container.Stats{\n\t\tName:         \"test-container\",\n\t\tCpu:          0.0,\n\t\tMem:          0.0,\n\t\tNetworkSent:  0.0,\n\t\tNetworkRecv:  0.0,\n\t\tPrevReadTime: time.Time{},\n\t}\n\n\ttestTime := time.Now()\n\tupdateContainerStatsValues(stats, 75.5, 1048576, 524288, 262144, testTime)\n\n\t// Check CPU percentage (should be rounded to 2 decimals)\n\tassert.Equal(t, 75.5, stats.Cpu)\n\n\t// Check memory (should be converted to MB: 1048576 bytes = 1 MB)\n\tassert.Equal(t, 1.0, stats.Mem)\n\n\t// Check bandwidth (raw bytes)\n\tassert.Equal(t, [2]uint64{524288, 262144}, stats.Bandwidth)\n\n\t// Deprecated fields still populated for backward compatibility with older hubs\n\tassert.Equal(t, 0.5, stats.NetworkSent)  // 524288 bytes = 0.5 MB\n\tassert.Equal(t, 0.25, stats.NetworkRecv) // 262144 bytes = 0.25 MB\n\n\t// Check read time\n\tassert.Equal(t, testTime, stats.PrevReadTime)\n}\n\nfunc TestInitializeCpuTracking(t *testing.T) {\n\tdm := &dockerManager{\n\t\tlastCpuContainer: make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:    make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:  make(map[uint16]map[string]time.Time),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Test initializing a new cache time\n\tdm.initializeCpuTracking(cacheTimeMs)\n\n\t// Check that maps were created\n\tassert.NotNil(t, dm.lastCpuContainer[cacheTimeMs])\n\tassert.NotNil(t, dm.lastCpuSystem[cacheTimeMs])\n\tassert.NotNil(t, dm.lastCpuReadTime[cacheTimeMs])\n\tassert.Empty(t, dm.lastCpuContainer[cacheTimeMs])\n\tassert.Empty(t, dm.lastCpuSystem[cacheTimeMs])\n\n\t// Test initializing existing cache time (should not overwrite)\n\tdm.lastCpuContainer[cacheTimeMs][\"test\"] = 100\n\tdm.lastCpuSystem[cacheTimeMs][\"test\"] = 200\n\n\tdm.initializeCpuTracking(cacheTimeMs)\n\n\t// Should still have the existing values\n\tassert.Equal(t, uint64(100), dm.lastCpuContainer[cacheTimeMs][\"test\"])\n\tassert.Equal(t, uint64(200), dm.lastCpuSystem[cacheTimeMs][\"test\"])\n}\n\nfunc TestGetCpuPreviousValues(t *testing.T) {\n\tdm := &dockerManager{\n\t\tlastCpuContainer: map[uint16]map[string]uint64{\n\t\t\t30000: {\"container1\": 100, \"container2\": 200},\n\t\t},\n\t\tlastCpuSystem: map[uint16]map[string]uint64{\n\t\t\t30000: {\"container1\": 150, \"container2\": 250},\n\t\t},\n\t}\n\n\t// Test getting existing values\n\tcontainer, system := dm.getCpuPreviousValues(30000, \"container1\")\n\tassert.Equal(t, uint64(100), container)\n\tassert.Equal(t, uint64(150), system)\n\n\t// Test getting non-existing container\n\tcontainer, system = dm.getCpuPreviousValues(30000, \"nonexistent\")\n\tassert.Equal(t, uint64(0), container)\n\tassert.Equal(t, uint64(0), system)\n\n\t// Test getting non-existing cache time\n\tcontainer, system = dm.getCpuPreviousValues(60000, \"container1\")\n\tassert.Equal(t, uint64(0), container)\n\tassert.Equal(t, uint64(0), system)\n}\n\nfunc TestSetCpuCurrentValues(t *testing.T) {\n\tdm := &dockerManager{\n\t\tlastCpuContainer: make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:    make(map[uint16]map[string]uint64),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\tcontainerId := \"test-container\"\n\n\t// Initialize the cache time maps first\n\tdm.initializeCpuTracking(cacheTimeMs)\n\n\t// Set values\n\tdm.setCpuCurrentValues(cacheTimeMs, containerId, 500, 750)\n\n\t// Check that values were set\n\tassert.Equal(t, uint64(500), dm.lastCpuContainer[cacheTimeMs][containerId])\n\tassert.Equal(t, uint64(750), dm.lastCpuSystem[cacheTimeMs][containerId])\n}\n\nfunc TestCalculateNetworkStats(t *testing.T) {\n\t// Create docker manager with tracker maps\n\tdm := &dockerManager{\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Pre-populate tracker for this cache time with initial values\n\tsentTracker := deltatracker.NewDeltaTracker[string, uint64]()\n\trecvTracker := deltatracker.NewDeltaTracker[string, uint64]()\n\tsentTracker.Set(\"container1\", 1000)\n\trecvTracker.Set(\"container1\", 800)\n\tsentTracker.Cycle() // Move to previous\n\trecvTracker.Cycle()\n\n\tdm.networkSentTrackers[cacheTimeMs] = sentTracker\n\tdm.networkRecvTrackers[cacheTimeMs] = recvTracker\n\n\tctr := &container.ApiInfo{\n\t\tIdShort: \"container1\",\n\t}\n\n\tapiStats := &container.ApiStats{\n\t\tNetworks: map[string]container.NetworkStats{\n\t\t\t\"eth0\": {TxBytes: 2000, RxBytes: 1800}, // New values\n\t\t},\n\t}\n\n\tstats := &container.Stats{\n\t\tPrevReadTime: time.Now().Add(-time.Second), // 1 second ago\n\t}\n\n\t// Test with initialized container\n\tsent, recv := dm.calculateNetworkStats(ctr, apiStats, stats, true, \"test-container\", cacheTimeMs)\n\n\t// Should return calculated byte rates per second\n\tassert.GreaterOrEqual(t, sent, uint64(0))\n\tassert.GreaterOrEqual(t, recv, uint64(0))\n\n\t// Cycle and test one-direction change (Tx only) is reflected independently\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\tapiStats.Networks[\"eth0\"] = container.NetworkStats{TxBytes: 2500, RxBytes: 1800} // +500 Tx only\n\tsent, recv = dm.calculateNetworkStats(ctr, apiStats, stats, true, \"test-container\", cacheTimeMs)\n\tassert.Greater(t, sent, uint64(0))\n\tassert.Equal(t, uint64(0), recv)\n}\n\nfunc TestDockerManagerCreation(t *testing.T) {\n\t// Test that dockerManager can be created without panicking\n\tdm := &dockerManager{\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:     make(map[uint16]map[string]time.Time),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tassert.NotNil(t, dm)\n\tassert.NotNil(t, dm.lastCpuContainer)\n\tassert.NotNil(t, dm.lastCpuSystem)\n\tassert.NotNil(t, dm.networkSentTrackers)\n\tassert.NotNil(t, dm.networkRecvTrackers)\n}\n\nfunc TestCheckDockerVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tresponses []struct {\n\t\t\tstatusCode int\n\t\t\tbody       string\n\t\t}\n\t\texpectedGood     bool\n\t\texpectedRequests int\n\t}{\n\t\t{\n\t\t\tname: \"200 with good version on first try\",\n\t\t\tresponses: []struct {\n\t\t\t\tstatusCode int\n\t\t\t\tbody       string\n\t\t\t}{\n\t\t\t\t{http.StatusOK, `{\"Version\":\"25.0.1\"}`},\n\t\t\t},\n\t\t\texpectedGood:     true,\n\t\t\texpectedRequests: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"200 with old version on first try\",\n\t\t\tresponses: []struct {\n\t\t\t\tstatusCode int\n\t\t\t\tbody       string\n\t\t\t}{\n\t\t\t\t{http.StatusOK, `{\"Version\":\"24.0.7\"}`},\n\t\t\t},\n\t\t\texpectedGood:     false,\n\t\t\texpectedRequests: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"non-200 then 200 with good version\",\n\t\t\tresponses: []struct {\n\t\t\t\tstatusCode int\n\t\t\t\tbody       string\n\t\t\t}{\n\t\t\t\t{http.StatusServiceUnavailable, `\"not ready\"`},\n\t\t\t\t{http.StatusOK, `{\"Version\":\"25.1.0\"}`},\n\t\t\t},\n\t\t\texpectedGood:     true,\n\t\t\texpectedRequests: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"non-200 on all retries\",\n\t\t\tresponses: []struct {\n\t\t\t\tstatusCode int\n\t\t\t\tbody       string\n\t\t\t}{\n\t\t\t\t{http.StatusInternalServerError, `\"error\"`},\n\t\t\t\t{http.StatusUnauthorized, `\"error\"`},\n\t\t\t},\n\t\t\texpectedGood:     false,\n\t\t\texpectedRequests: 2,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\trequestCount := 0\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tidx := requestCount\n\t\t\t\trequestCount++\n\t\t\t\tif idx >= len(tt.responses) {\n\t\t\t\t\tidx = len(tt.responses) - 1\n\t\t\t\t}\n\t\t\t\tw.WriteHeader(tt.responses[idx].statusCode)\n\t\t\t\tfmt.Fprint(w, tt.responses[idx].body)\n\t\t\t}))\n\t\t\tdefer server.Close()\n\n\t\t\tdm := &dockerManager{\n\t\t\t\tclient: &http.Client{\n\t\t\t\t\tTransport: &http.Transport{\n\t\t\t\t\t\tDialContext: func(_ context.Context, network, _ string) (net.Conn, error) {\n\t\t\t\t\t\t\treturn net.Dial(network, server.Listener.Addr().String())\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tretrySleep: func(time.Duration) {},\n\t\t\t}\n\n\t\t\tdm.checkDockerVersion()\n\n\t\t\tassert.Equal(t, tt.expectedGood, dm.goodDockerVersion)\n\t\t\tassert.Equal(t, tt.expectedRequests, requestCount)\n\t\t})\n\t}\n\n\tt.Run(\"request error on all retries\", func(t *testing.T) {\n\t\trequestCount := 0\n\t\tdm := &dockerManager{\n\t\t\tclient: &http.Client{\n\t\t\t\tTransport: &http.Transport{\n\t\t\t\t\tDialContext: func(_ context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\t\t\trequestCount++\n\t\t\t\t\t\treturn nil, errors.New(\"connection refused\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tretrySleep: func(time.Duration) {},\n\t\t}\n\n\t\tdm.checkDockerVersion()\n\n\t\tassert.False(t, dm.goodDockerVersion)\n\t\tassert.Equal(t, 2, requestCount)\n\t})\n}\n\nfunc TestCycleCpuDeltas(t *testing.T) {\n\tdm := &dockerManager{\n\t\tlastCpuContainer: map[uint16]map[string]uint64{\n\t\t\t30000: {\"container1\": 100, \"container2\": 200},\n\t\t},\n\t\tlastCpuSystem: map[uint16]map[string]uint64{\n\t\t\t30000: {\"container1\": 150, \"container2\": 250},\n\t\t},\n\t\tlastCpuReadTime: map[uint16]map[string]time.Time{\n\t\t\t30000: {\"container1\": time.Now()},\n\t\t},\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Verify values exist before cycling\n\tassert.Equal(t, uint64(100), dm.lastCpuContainer[cacheTimeMs][\"container1\"])\n\tassert.Equal(t, uint64(200), dm.lastCpuContainer[cacheTimeMs][\"container2\"])\n\n\t// Cycle the CPU deltas\n\tdm.cycleCpuDeltas(cacheTimeMs)\n\n\t// Verify values are cleared\n\tassert.Empty(t, dm.lastCpuContainer[cacheTimeMs])\n\tassert.Empty(t, dm.lastCpuSystem[cacheTimeMs])\n\t// lastCpuReadTime is not affected by cycleCpuDeltas\n\tassert.NotEmpty(t, dm.lastCpuReadTime[cacheTimeMs])\n}\n\nfunc TestCycleNetworkDeltas(t *testing.T) {\n\t// Create docker manager with tracker maps\n\tdm := &dockerManager{\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Get trackers for this cache time (creates them)\n\tsentTracker := dm.getNetworkTracker(cacheTimeMs, true)\n\trecvTracker := dm.getNetworkTracker(cacheTimeMs, false)\n\n\t// Set some test data\n\tsentTracker.Set(\"test\", 100)\n\trecvTracker.Set(\"test\", 200)\n\n\t// This should not panic\n\tassert.NotPanics(t, func() {\n\t\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\t})\n\n\t// Verify that cycle worked by checking deltas are now zero (no previous values)\n\tassert.Equal(t, uint64(0), sentTracker.Delta(\"test\"))\n\tassert.Equal(t, uint64(0), recvTracker.Delta(\"test\"))\n}\n\nfunc TestConstants(t *testing.T) {\n\t// Test that constants are properly defined\n\tassert.Equal(t, uint16(60000), defaultCacheTimeMs)\n\tassert.Equal(t, uint64(5e9), maxNetworkSpeedBps)\n\tassert.Equal(t, 2100, dockerTimeoutMs)\n}\n\nfunc TestDockerStatsWithMockData(t *testing.T) {\n\t// Create a docker manager with initialized tracking\n\tdm := &dockerManager{\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:     make(map[uint16]map[string]time.Time),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tcontainerStatsMap:   make(map[string]*container.Stats),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Test that initializeCpuTracking works\n\tdm.initializeCpuTracking(cacheTimeMs)\n\tassert.NotNil(t, dm.lastCpuContainer[cacheTimeMs])\n\tassert.NotNil(t, dm.lastCpuSystem[cacheTimeMs])\n\n\t// Test that we can set and get CPU values\n\tdm.setCpuCurrentValues(cacheTimeMs, \"test-container\", 1000, 2000)\n\tcontainer, system := dm.getCpuPreviousValues(cacheTimeMs, \"test-container\")\n\tassert.Equal(t, uint64(1000), container)\n\tassert.Equal(t, uint64(2000), system)\n}\n\nfunc TestMemoryStatsEdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tusage     uint64\n\t\tcache     uint64\n\t\tinactive  uint64\n\t\tisWindows bool\n\t\texpected  uint64\n\t\thasError  bool\n\t}{\n\t\t{\"Linux normal case\", 1000, 200, 0, false, 800, false},\n\t\t{\"Linux with inactive file\", 1000, 0, 300, false, 700, false},\n\t\t{\"Windows normal case\", 0, 0, 0, true, 500, false},\n\t\t{\"Linux zero usage error\", 0, 0, 0, false, 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapiStats := &container.ApiStats{\n\t\t\t\tMemoryStats: container.MemoryStats{\n\t\t\t\t\tUsage: tt.usage,\n\t\t\t\t\tStats: container.MemoryStatsStats{\n\t\t\t\t\t\tCache:        tt.cache,\n\t\t\t\t\t\tInactiveFile: tt.inactive,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tif tt.isWindows {\n\t\t\t\tapiStats.MemoryStats.PrivateWorkingSet = tt.expected\n\t\t\t}\n\n\t\t\tresult, err := calculateMemoryUsage(apiStats, tt.isWindows)\n\n\t\t\tif tt.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContainerStatsInitialization(t *testing.T) {\n\tstats := &container.Stats{Name: \"test-container\"}\n\n\t// Verify initial values\n\tassert.Equal(t, \"test-container\", stats.Name)\n\tassert.Equal(t, 0.0, stats.Cpu)\n\tassert.Equal(t, 0.0, stats.Mem)\n\tassert.Equal(t, 0.0, stats.NetworkSent)\n\tassert.Equal(t, 0.0, stats.NetworkRecv)\n\tassert.Equal(t, time.Time{}, stats.PrevReadTime)\n\n\t// Test updating values\n\ttestTime := time.Now()\n\tupdateContainerStatsValues(stats, 45.67, 2097152, 1048576, 524288, testTime)\n\n\tassert.Equal(t, 45.67, stats.Cpu)\n\tassert.Equal(t, 2.0, stats.Mem)\n\tassert.Equal(t, [2]uint64{1048576, 524288}, stats.Bandwidth)\n\t// Deprecated fields still populated for backward compatibility with older hubs\n\tassert.Equal(t, 1.0, stats.NetworkSent) // 1048576 bytes = 1 MB\n\tassert.Equal(t, 0.5, stats.NetworkRecv) // 524288 bytes = 0.5 MB\n\tassert.Equal(t, testTime, stats.PrevReadTime)\n}\n\n// Test with real Docker API test data\nfunc TestCalculateMemoryUsageWithRealData(t *testing.T) {\n\t// Load minimal container stats from test data\n\tdata, err := os.ReadFile(\"test-data/container.json\")\n\trequire.NoError(t, err)\n\n\tvar apiStats container.ApiStats\n\terr = json.Unmarshal(data, &apiStats)\n\trequire.NoError(t, err)\n\n\t// Test memory calculation with real data\n\tusedMemory, err := calculateMemoryUsage(&apiStats, false)\n\trequire.NoError(t, err)\n\n\t// From the real data: usage - inactive_file = 507400192 - 165130240 = 342269952\n\texpected := uint64(507400192 - 165130240)\n\tassert.Equal(t, expected, usedMemory)\n}\n\nfunc TestCpuPercentageCalculationWithRealData(t *testing.T) {\n\t// Load minimal container stats from test data\n\tdata1, err := os.ReadFile(\"test-data/container.json\")\n\trequire.NoError(t, err)\n\n\tdata2, err := os.ReadFile(\"test-data/container2.json\")\n\trequire.NoError(t, err)\n\n\tvar apiStats1, apiStats2 container.ApiStats\n\terr = json.Unmarshal(data1, &apiStats1)\n\trequire.NoError(t, err)\n\terr = json.Unmarshal(data2, &apiStats2)\n\trequire.NoError(t, err)\n\n\t// Calculate delta manually: 314891801000 - 312055276000 = 2836525000\n\t// System delta: 1368474900000000 - 1366399830000000 = 2075070000000\n\t// Expected %: (2836525000 / 2075070000000) * 100 ≈ 0.1367%\n\texpectedPct := float64(2836525000) / float64(2075070000000) * 100.0\n\tactualPct := apiStats2.CalculateCpuPercentLinux(apiStats1.CPUStats.CPUUsage.TotalUsage, apiStats1.CPUStats.SystemUsage)\n\n\tassert.InDelta(t, expectedPct, actualPct, 0.01)\n}\n\nfunc TestNetworkStatsCalculationWithRealData(t *testing.T) {\n\t// Create synthetic test data to avoid timing issues\n\tapiStats1 := &container.ApiStats{\n\t\tNetworks: map[string]container.NetworkStats{\n\t\t\t\"eth0\": {TxBytes: 1000000, RxBytes: 500000},\n\t\t},\n\t}\n\n\tapiStats2 := &container.ApiStats{\n\t\tNetworks: map[string]container.NetworkStats{\n\t\t\t\"eth0\": {TxBytes: 3000000, RxBytes: 1500000}, // 2MB sent, 1MB received increase\n\t\t},\n\t}\n\n\t// Create docker manager with tracker maps\n\tdm := &dockerManager{\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tctr := &container.ApiInfo{IdShort: \"test-container\"}\n\tcacheTimeMs := uint16(30000) // Test with 30 second cache\n\n\t// Use exact timing for deterministic results\n\texactly1000msAgo := time.Now().Add(-1000 * time.Millisecond)\n\tstats := &container.Stats{\n\t\tPrevReadTime: exactly1000msAgo,\n\t}\n\n\t// First call sets baseline\n\tsent1, recv1 := dm.calculateNetworkStats(ctr, apiStats1, stats, true, \"test\", cacheTimeMs)\n\tassert.Equal(t, uint64(0), sent1)\n\tassert.Equal(t, uint64(0), recv1)\n\n\t// Cycle to establish baseline for this cache time\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\n\t// Calculate expected results precisely\n\tdeltaSent := uint64(2000000)                             // 3000000 - 1000000\n\tdeltaRecv := uint64(1000000)                             // 1500000 - 500000\n\texpectedElapsedMs := uint64(1000)                        // Exactly 1000ms\n\texpectedSentRate := deltaSent * 1000 / expectedElapsedMs // Should be exactly 2000000\n\texpectedRecvRate := deltaRecv * 1000 / expectedElapsedMs // Should be exactly 1000000\n\n\t// Second call with changed data\n\tsent2, recv2 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, \"test\", cacheTimeMs)\n\n\t// Should be exactly the expected rates (no tolerance needed)\n\tassert.Equal(t, expectedSentRate, sent2)\n\tassert.Equal(t, expectedRecvRate, recv2)\n\n\t// Bad speed cap: set absurd delta over 1ms and expect 0 due to cap\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\tstats.PrevReadTime = time.Now().Add(-1 * time.Millisecond)\n\tapiStats1.Networks[\"eth0\"] = container.NetworkStats{TxBytes: 0, RxBytes: 0}\n\tapiStats2.Networks[\"eth0\"] = container.NetworkStats{TxBytes: 10 * 1024 * 1024 * 1024, RxBytes: 0} // 10GB delta\n\t_, _ = dm.calculateNetworkStats(ctr, apiStats1, stats, true, \"test\", cacheTimeMs)                 // baseline\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\tsent3, recv3 := dm.calculateNetworkStats(ctr, apiStats2, stats, true, \"test\", cacheTimeMs)\n\tassert.Equal(t, uint64(0), sent3)\n\tassert.Equal(t, uint64(0), recv3)\n}\n\nfunc TestContainerStatsEndToEndWithRealData(t *testing.T) {\n\t// Load minimal container stats\n\tdata, err := os.ReadFile(\"test-data/container.json\")\n\trequire.NoError(t, err)\n\n\tvar apiStats container.ApiStats\n\terr = json.Unmarshal(data, &apiStats)\n\trequire.NoError(t, err)\n\n\t// Create a docker manager with proper initialization\n\tdm := &dockerManager{\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:     make(map[uint16]map[string]time.Time),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tcontainerStatsMap:   make(map[string]*container.Stats),\n\t}\n\n\t// Initialize CPU tracking\n\tcacheTimeMs := uint16(30000)\n\tdm.initializeCpuTracking(cacheTimeMs)\n\n\t// Create container info\n\tctr := &container.ApiInfo{\n\t\tIdShort: \"abc123\",\n\t}\n\n\t// Initialize container stats\n\tstats := &container.Stats{Name: \"jellyfin\"}\n\tdm.containerStatsMap[ctr.IdShort] = stats\n\n\t// Test individual components that we can verify\n\tusedMemory, memErr := calculateMemoryUsage(&apiStats, false)\n\tassert.NoError(t, memErr)\n\tassert.Greater(t, usedMemory, uint64(0))\n\n\t// Test CPU percentage validation\n\tcpuPct := 85.5\n\terr = validateCpuPercentage(cpuPct, \"jellyfin\")\n\tassert.NoError(t, err)\n\n\terr = validateCpuPercentage(150.0, \"jellyfin\")\n\tassert.Error(t, err)\n\n\t// Test stats value updates\n\ttestStats := &container.Stats{}\n\ttestTime := time.Now()\n\tupdateContainerStatsValues(testStats, cpuPct, usedMemory, 1000000, 500000, testTime)\n\n\tassert.Equal(t, cpuPct, testStats.Cpu)\n\tassert.Equal(t, utils.BytesToMegabytes(float64(usedMemory)), testStats.Mem)\n\tassert.Equal(t, [2]uint64{1000000, 500000}, testStats.Bandwidth)\n\t// Deprecated fields still populated for backward compatibility with older hubs\n\tassert.Equal(t, utils.BytesToMegabytes(1000000), testStats.NetworkSent)\n\tassert.Equal(t, utils.BytesToMegabytes(500000), testStats.NetworkRecv)\n\tassert.Equal(t, testTime, testStats.PrevReadTime)\n}\n\nfunc TestGetLogsDetectsMultiplexedWithoutContentType(t *testing.T) {\n\t// Docker multiplexed frame: [stream][0,0,0][len(4 bytes BE)][payload]\n\tframe := []byte{\n\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,\n\t\t'H', 'e', 'l', 'l', 'o',\n\t}\n\trt := &recordingRoundTripper{\n\t\tstatusCode: 200,\n\t\tbody:       string(frame),\n\t\t// Intentionally omit content type to simulate Podman behavior.\n\t}\n\tdm := &dockerManager{\n\t\tclient: &http.Client{Transport: rt},\n\t}\n\n\tlogs, err := dm.getLogs(context.Background(), \"abcdef123456\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"Hello\", logs)\n}\n\nfunc TestGetLogsDoesNotMisclassifyRawStreamAsMultiplexed(t *testing.T) {\n\t// Starts with 0x01, but doesn't match Docker frame signature (reserved bytes aren't all zero).\n\traw := []byte{0x01, 0x02, 0x03, 0x04, 'r', 'a', 'w'}\n\trt := &recordingRoundTripper{\n\t\tstatusCode: 200,\n\t\tbody:       string(raw),\n\t}\n\tdm := &dockerManager{\n\t\tclient: &http.Client{Transport: rt},\n\t}\n\n\tlogs, err := dm.getLogs(context.Background(), \"abcdef123456\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, raw, []byte(logs))\n}\n\nfunc TestEdgeCasesWithRealData(t *testing.T) {\n\t// Test with minimal container stats\n\tminimalStats := &container.ApiStats{\n\t\tCPUStats: container.CPUStats{\n\t\t\tCPUUsage:    container.CPUUsage{TotalUsage: 1000},\n\t\t\tSystemUsage: 50000,\n\t\t},\n\t\tMemoryStats: container.MemoryStats{\n\t\t\tUsage: 1000000,\n\t\t\tStats: container.MemoryStatsStats{\n\t\t\t\tCache:        0,\n\t\t\t\tInactiveFile: 0,\n\t\t\t},\n\t\t},\n\t\tNetworks: map[string]container.NetworkStats{\n\t\t\t\"eth0\": {TxBytes: 1000, RxBytes: 500},\n\t\t},\n\t}\n\n\t// Test memory calculation with zero cache/inactive\n\tusedMemory, err := calculateMemoryUsage(minimalStats, false)\n\tassert.NoError(t, err)\n\tassert.Equal(t, uint64(1000000), usedMemory) // Should equal usage when no cache\n\n\t// Test CPU percentage calculation\n\tcpuPct := minimalStats.CalculateCpuPercentLinux(0, 0) // First run\n\tassert.Equal(t, 0.0, cpuPct)\n\n\t// Test with Windows data\n\tminimalStats.MemoryStats.PrivateWorkingSet = 800000\n\tusedMemory, err = calculateMemoryUsage(minimalStats, true)\n\tassert.NoError(t, err)\n\tassert.Equal(t, uint64(800000), usedMemory)\n}\n\nfunc TestDockerStatsWorkflow(t *testing.T) {\n\t// Test the complete workflow that can be tested without HTTP calls\n\tdm := &dockerManager{\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tcontainerStatsMap:   make(map[string]*container.Stats),\n\t}\n\n\tcacheTimeMs := uint16(30000)\n\n\t// Test CPU tracking workflow\n\tdm.initializeCpuTracking(cacheTimeMs)\n\tassert.NotNil(t, dm.lastCpuContainer[cacheTimeMs])\n\n\t// Test setting and getting CPU values\n\tdm.setCpuCurrentValues(cacheTimeMs, \"test-container\", 1000, 50000)\n\tcontainerVal, systemVal := dm.getCpuPreviousValues(cacheTimeMs, \"test-container\")\n\tassert.Equal(t, uint64(1000), containerVal)\n\tassert.Equal(t, uint64(50000), systemVal)\n\n\t// Test network tracking workflow (multi-interface summation)\n\tsentTracker := dm.getNetworkTracker(cacheTimeMs, true)\n\trecvTracker := dm.getNetworkTracker(cacheTimeMs, false)\n\n\t// Simulate two interfaces summed by setting combined totals\n\tsentTracker.Set(\"test-container\", 1000+2000)\n\trecvTracker.Set(\"test-container\", 500+700)\n\n\tdeltaSent := sentTracker.Delta(\"test-container\")\n\tdeltaRecv := recvTracker.Delta(\"test-container\")\n\tassert.Equal(t, uint64(0), deltaSent) // No previous value\n\tassert.Equal(t, uint64(0), deltaRecv)\n\n\t// Cycle and test again\n\tdm.cycleNetworkDeltasForCacheTime(cacheTimeMs)\n\n\t// Increase each interface total (combined totals go up by 1500 and 800)\n\tsentTracker.Set(\"test-container\", (1000+2000)+1500)\n\trecvTracker.Set(\"test-container\", (500+700)+800)\n\n\tdeltaSent = sentTracker.Delta(\"test-container\")\n\tdeltaRecv = recvTracker.Delta(\"test-container\")\n\tassert.Equal(t, uint64(1500), deltaSent)\n\tassert.Equal(t, uint64(800), deltaRecv)\n}\n\nfunc TestNetworkRateCalculationFormula(t *testing.T) {\n\t// Test the exact formula used in calculateNetworkStats\n\ttestCases := []struct {\n\t\tname         string\n\t\tdeltaBytes   uint64\n\t\telapsedMs    uint64\n\t\texpectedRate uint64\n\t}{\n\t\t{\"1MB over 1 second\", 1000000, 1000, 1000000},\n\t\t{\"2MB over 1 second\", 2000000, 1000, 2000000},\n\t\t{\"1MB over 2 seconds\", 1000000, 2000, 500000},\n\t\t{\"500KB over 500ms\", 500000, 500, 1000000},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// This is the exact formula from calculateNetworkStats\n\t\t\tactualRate := tc.deltaBytes * 1000 / tc.elapsedMs\n\t\t\tassert.Equal(t, tc.expectedRate, actualRate,\n\t\t\t\t\"Rate calculation should be exact: %d bytes * 1000 / %d ms = %d\",\n\t\t\t\ttc.deltaBytes, tc.elapsedMs, tc.expectedRate)\n\t\t})\n\t}\n}\n\nfunc TestGetHostInfo(t *testing.T) {\n\tdata, err := os.ReadFile(\"test-data/system_info.json\")\n\trequire.NoError(t, err)\n\n\tvar info container.HostInfo\n\terr = json.Unmarshal(data, &info)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"6.8.0-31-generic\", info.KernelVersion)\n\tassert.Equal(t, \"Ubuntu 24.04 LTS\", info.OperatingSystem)\n\t// assert.Equal(t, \"24.04\", info.OSVersion)\n\t// assert.Equal(t, \"linux\", info.OSType)\n\t// assert.Equal(t, \"x86_64\", info.Architecture)\n\tassert.EqualValues(t, 4, info.NCPU)\n\tassert.EqualValues(t, 2095882240, info.MemTotal)\n\t// assert.Equal(t, \"27.0.1\", info.ServerVersion)\n}\n\nfunc TestDeltaTrackerCacheTimeIsolation(t *testing.T) {\n\t// Test that different cache times have separate DeltaTracker instances\n\tdm := &dockerManager{\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tctr := &container.ApiInfo{IdShort: \"web-server\"}\n\tcacheTime1 := uint16(30000)\n\tcacheTime2 := uint16(60000)\n\n\t// Get trackers for different cache times (creates separate instances)\n\tsentTracker1 := dm.getNetworkTracker(cacheTime1, true)\n\trecvTracker1 := dm.getNetworkTracker(cacheTime1, false)\n\n\tsentTracker2 := dm.getNetworkTracker(cacheTime2, true)\n\trecvTracker2 := dm.getNetworkTracker(cacheTime2, false)\n\n\t// Verify they are different instances\n\tassert.NotSame(t, sentTracker1, sentTracker2)\n\tassert.NotSame(t, recvTracker1, recvTracker2)\n\n\t// Set values for cache time 1\n\tsentTracker1.Set(ctr.IdShort, 1000000)\n\trecvTracker1.Set(ctr.IdShort, 500000)\n\n\t// Set values for cache time 2\n\tsentTracker2.Set(ctr.IdShort, 2000000)\n\trecvTracker2.Set(ctr.IdShort, 1000000)\n\n\t// Verify they don't interfere (both should return 0 since no previous values)\n\tassert.Equal(t, uint64(0), sentTracker1.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(0), recvTracker1.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(0), sentTracker2.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(0), recvTracker2.Delta(ctr.IdShort))\n\n\t// Cycle cache time 1 trackers\n\tdm.cycleNetworkDeltasForCacheTime(cacheTime1)\n\n\t// Set new values for cache time 1\n\tsentTracker1.Set(ctr.IdShort, 3000000) // 2MB increase\n\trecvTracker1.Set(ctr.IdShort, 1500000) // 1MB increase\n\n\t// Cache time 1 should show deltas, cache time 2 should still be 0\n\tassert.Equal(t, uint64(2000000), sentTracker1.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(1000000), recvTracker1.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(0), sentTracker2.Delta(ctr.IdShort)) // Unaffected\n\tassert.Equal(t, uint64(0), recvTracker2.Delta(ctr.IdShort)) // Unaffected\n\n\t// Cycle cache time 2 and verify it works independently\n\tdm.cycleNetworkDeltasForCacheTime(cacheTime2)\n\tsentTracker2.Set(ctr.IdShort, 2500000) // 0.5MB increase\n\trecvTracker2.Set(ctr.IdShort, 1200000) // 0.2MB increase\n\n\tassert.Equal(t, uint64(500000), sentTracker2.Delta(ctr.IdShort))\n\tassert.Equal(t, uint64(200000), recvTracker2.Delta(ctr.IdShort))\n}\n\nfunc TestParseDockerStatus(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tinput          string\n\t\texpectedStatus string\n\t\texpectedHealth container.DockerHealth\n\t}{\n\t\t{\n\t\t\tname:           \"status with About an removed\",\n\t\t\tinput:          \"Up About an hour (healthy)\",\n\t\t\texpectedStatus: \"Up an hour\",\n\t\t\texpectedHealth: container.DockerHealthHealthy,\n\t\t},\n\t\t{\n\t\t\tname:           \"status without About an unchanged\",\n\t\t\tinput:          \"Up 2 hours (healthy)\",\n\t\t\texpectedStatus: \"Up 2 hours\",\n\t\t\texpectedHealth: container.DockerHealthHealthy,\n\t\t},\n\t\t{\n\t\t\tname:           \"status with About and no parentheses\",\n\t\t\tinput:          \"Up About an hour\",\n\t\t\texpectedStatus: \"Up an hour\",\n\t\t\texpectedHealth: container.DockerHealthNone,\n\t\t},\n\t\t{\n\t\t\tname:           \"status without parentheses\",\n\t\t\tinput:          \"Created\",\n\t\t\texpectedStatus: \"Created\",\n\t\t\texpectedHealth: container.DockerHealthNone,\n\t\t},\n\t\t{\n\t\t\tname:           \"empty status\",\n\t\t\tinput:          \"\",\n\t\t\texpectedStatus: \"\",\n\t\t\texpectedHealth: container.DockerHealthNone,\n\t\t},\n\t\t{\n\t\t\tname:           \"status health with health: prefix\",\n\t\t\tinput:          \"Up 5 minutes (health: starting)\",\n\t\t\texpectedStatus: \"Up 5 minutes\",\n\t\t\texpectedHealth: container.DockerHealthStarting,\n\t\t},\n\t\t{\n\t\t\tname:           \"status health with health status: prefix\",\n\t\t\tinput:          \"Up 10 minutes (health status: unhealthy)\",\n\t\t\texpectedStatus: \"Up 10 minutes\",\n\t\t\texpectedHealth: container.DockerHealthUnhealthy,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstatus, health := parseDockerStatus(tt.input)\n\t\t\tassert.Equal(t, tt.expectedStatus, status)\n\t\t\tassert.Equal(t, tt.expectedHealth, health)\n\t\t})\n\t}\n}\n\nfunc TestParseDockerHealthStatus(t *testing.T) {\n\ttests := []struct {\n\t\tinput          string\n\t\texpectedHealth container.DockerHealth\n\t\texpectedOk     bool\n\t}{\n\t\t{\"healthy\", container.DockerHealthHealthy, true},\n\t\t{\"unhealthy\", container.DockerHealthUnhealthy, true},\n\t\t{\"starting\", container.DockerHealthStarting, true},\n\t\t{\"none\", container.DockerHealthNone, true},\n\t\t{\" Healthy \", container.DockerHealthHealthy, true},\n\t\t{\"unknown\", container.DockerHealthNone, false},\n\t\t{\"\", container.DockerHealthNone, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\thealth, ok := parseDockerHealthStatus(tt.input)\n\t\t\tassert.Equal(t, tt.expectedHealth, health)\n\t\t\tassert.Equal(t, tt.expectedOk, ok)\n\t\t})\n\t}\n}\n\nfunc TestUpdateContainerStatsUsesPodmanInspectHealthFallback(t *testing.T) {\n\tvar requestedPaths []string\n\tdm := &dockerManager{\n\t\tclient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {\n\t\t\trequestedPaths = append(requestedPaths, req.URL.EscapedPath())\n\t\t\tswitch req.URL.EscapedPath() {\n\t\t\tcase \"/containers/0123456789ab/stats\":\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusOK,\n\t\t\t\t\tStatus:     \"200 OK\",\n\t\t\t\t\tHeader:     make(http.Header),\n\t\t\t\t\tBody: io.NopCloser(strings.NewReader(`{\n\t\t\t\t\t\t\"read\":\"2026-03-15T21:26:59Z\",\n\t\t\t\t\t\t\"cpu_stats\":{\"cpu_usage\":{\"total_usage\":1000},\"system_cpu_usage\":2000},\n\t\t\t\t\t\t\"memory_stats\":{\"usage\":1048576,\"stats\":{\"inactive_file\":262144}},\n\t\t\t\t\t\t\"networks\":{\"eth0\":{\"rx_bytes\":0,\"tx_bytes\":0}}\n\t\t\t\t\t}`)),\n\t\t\t\t\tRequest: req,\n\t\t\t\t}, nil\n\t\t\tcase \"/containers/0123456789ab/json\":\n\t\t\t\treturn &http.Response{\n\t\t\t\t\tStatusCode: http.StatusOK,\n\t\t\t\t\tStatus:     \"200 OK\",\n\t\t\t\t\tHeader:     make(http.Header),\n\t\t\t\t\tBody:       io.NopCloser(strings.NewReader(`{\"State\":{\"Health\":{\"Status\":\"healthy\"}}}`)),\n\t\t\t\t\tRequest:    req,\n\t\t\t\t}, nil\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected path: %s\", req.URL.EscapedPath())\n\t\t\t}\n\t\t})},\n\t\tcontainerStatsMap:   make(map[string]*container.Stats),\n\t\tapiStats:            &container.ApiStats{},\n\t\tusingPodman:         true,\n\t\tlastCpuContainer:    make(map[uint16]map[string]uint64),\n\t\tlastCpuSystem:       make(map[uint16]map[string]uint64),\n\t\tlastCpuReadTime:     make(map[uint16]map[string]time.Time),\n\t\tnetworkSentTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\tnetworkRecvTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tctr := &container.ApiInfo{\n\t\tIdShort: \"0123456789ab\",\n\t\tNames:   []string{\"/beszel\"},\n\t\tStatus:  \"Up 2 minutes\",\n\t\tImage:   \"beszel:latest\",\n\t}\n\n\terr := dm.updateContainerStats(ctr, defaultCacheTimeMs)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"/containers/0123456789ab/stats\", \"/containers/0123456789ab/json\"}, requestedPaths)\n\tassert.Equal(t, container.DockerHealthHealthy, dm.containerStatsMap[ctr.IdShort].Health)\n\tassert.Equal(t, \"Up 2 minutes\", dm.containerStatsMap[ctr.IdShort].Status)\n}\n\nfunc TestConstantsAndUtilityFunctions(t *testing.T) {\n\t// Test constants are properly defined\n\tassert.Equal(t, uint16(60000), defaultCacheTimeMs)\n\tassert.Equal(t, uint64(5e9), maxNetworkSpeedBps)\n\tassert.Equal(t, 2100, dockerTimeoutMs)\n\tassert.Equal(t, uint32(1024*1024), uint32(maxLogFrameSize)) // 1MB\n\tassert.Equal(t, 5*1024*1024, maxTotalLogSize)               // 5MB\n\n\t// Test utility functions\n\tassert.Equal(t, 1.5, utils.TwoDecimals(1.499))\n\tassert.Equal(t, 1.5, utils.TwoDecimals(1.5))\n\tassert.Equal(t, 1.5, utils.TwoDecimals(1.501))\n\n\tassert.Equal(t, 1.0, utils.BytesToMegabytes(1048576)) // 1 MB\n\tassert.Equal(t, 0.5, utils.BytesToMegabytes(524288))  // 512 KB\n\tassert.Equal(t, 0.0, utils.BytesToMegabytes(0))\n}\n\nfunc TestDecodeDockerLogStream(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       []byte\n\t\texpected    string\n\t\texpectError bool\n\t\tmultiplexed bool\n\t}{\n\t\t{\n\t\t\tname: \"simple log entry\",\n\t\t\tinput: []byte{\n\t\t\t\t// Frame 1: stdout, 11 bytes\n\t\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B,\n\t\t\t\t'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd',\n\t\t\t},\n\t\t\texpected:    \"Hello World\",\n\t\t\texpectError: false,\n\t\t\tmultiplexed: true,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple frames\",\n\t\t\tinput: []byte{\n\t\t\t\t// Frame 1: stdout, 5 bytes\n\t\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,\n\t\t\t\t'H', 'e', 'l', 'l', 'o',\n\t\t\t\t// Frame 2: stdout, 5 bytes\n\t\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,\n\t\t\t\t'W', 'o', 'r', 'l', 'd',\n\t\t\t},\n\t\t\texpected:    \"HelloWorld\",\n\t\t\texpectError: false,\n\t\t\tmultiplexed: true,\n\t\t},\n\t\t{\n\t\t\tname: \"zero length frame\",\n\t\t\tinput: []byte{\n\t\t\t\t// Frame 1: stdout, 0 bytes\n\t\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t\t\t// Frame 2: stdout, 5 bytes\n\t\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,\n\t\t\t\t'H', 'e', 'l', 'l', 'o',\n\t\t\t},\n\t\t\texpected:    \"Hello\",\n\t\t\texpectError: false,\n\t\t\tmultiplexed: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty input\",\n\t\t\tinput:       []byte{},\n\t\t\texpected:    \"\",\n\t\t\texpectError: false,\n\t\t\tmultiplexed: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"raw stream (not multiplexed)\",\n\t\t\tinput:       []byte(\"raw log content\"),\n\t\t\texpected:    \"raw log content\",\n\t\t\tmultiplexed: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treader := bytes.NewReader(tt.input)\n\t\t\tvar builder strings.Builder\n\t\t\terr := decodeDockerLogStream(reader, &builder, tt.multiplexed)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, builder.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {\n\tt.Run(\"excessively large frame should error\", func(t *testing.T) {\n\t\t// Create a frame with size exceeding maxLogFrameSize\n\t\texcessiveSize := uint32(maxLogFrameSize + 1)\n\t\tinput := []byte{\n\t\t\t// Frame header with excessive size\n\t\t\t0x01, 0x00, 0x00, 0x00,\n\t\t\tbyte(excessiveSize >> 24), byte(excessiveSize >> 16), byte(excessiveSize >> 8), byte(excessiveSize),\n\t\t}\n\n\t\treader := bytes.NewReader(input)\n\t\tvar builder strings.Builder\n\t\terr := decodeDockerLogStream(reader, &builder, true)\n\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"log frame size\")\n\t\tassert.Contains(t, err.Error(), \"exceeds maximum\")\n\t})\n\n\tt.Run(\"total size limit should truncate\", func(t *testing.T) {\n\t\t// Create frames that exceed maxTotalLogSize (5MB)\n\t\t// Use frames within maxLogFrameSize (1MB) to avoid single-frame rejection\n\t\tframeSize := uint32(800 * 1024) // 800KB per frame\n\t\tvar input []byte\n\n\t\t// Frames 1-6: 800KB each (total 4.8MB - within 5MB limit)\n\t\tfor i := 0; i < 6; i++ {\n\t\t\tchar := byte('A' + i)\n\t\t\tframeHeader := []byte{\n\t\t\t\t0x01, 0x00, 0x00, 0x00,\n\t\t\t\tbyte(frameSize >> 24), byte(frameSize >> 16), byte(frameSize >> 8), byte(frameSize),\n\t\t\t}\n\t\t\tinput = append(input, frameHeader...)\n\t\t\tinput = append(input, bytes.Repeat([]byte{char}, int(frameSize))...)\n\t\t}\n\n\t\t// Frame 7: 800KB (would bring total to 5.6MB, exceeding 5MB limit - should be truncated)\n\t\tframe7Header := []byte{\n\t\t\t0x01, 0x00, 0x00, 0x00,\n\t\t\tbyte(frameSize >> 24), byte(frameSize >> 16), byte(frameSize >> 8), byte(frameSize),\n\t\t}\n\t\tinput = append(input, frame7Header...)\n\t\tinput = append(input, bytes.Repeat([]byte{'Z'}, int(frameSize))...)\n\n\t\treader := bytes.NewReader(input)\n\t\tvar builder strings.Builder\n\t\terr := decodeDockerLogStream(reader, &builder, true)\n\n\t\t// Should complete without error (graceful truncation)\n\t\tassert.NoError(t, err)\n\t\t// Should have read 6 frames (4.8MB total, stopping before 7th would exceed 5MB limit)\n\t\texpectedSize := int(frameSize) * 6\n\t\tassert.Equal(t, expectedSize, builder.Len())\n\t\t// Should contain A-F but not Z\n\t\tresult := builder.String()\n\t\tassert.Contains(t, result, \"A\")\n\t\tassert.Contains(t, result, \"F\")\n\t\tassert.NotContains(t, result, \"Z\")\n\t})\n}\n\nfunc TestShouldExcludeContainer(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tcontainerName string\n\t\tpatterns      []string\n\t\texpected      bool\n\t}{\n\t\t{\n\t\t\tname:          \"empty patterns excludes nothing\",\n\t\t\tcontainerName: \"any-container\",\n\t\t\tpatterns:      []string{},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exact match - excluded\",\n\t\t\tcontainerName: \"test-web\",\n\t\t\tpatterns:      []string{\"test-web\", \"test-api\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"exact match - not excluded\",\n\t\t\tcontainerName: \"prod-web\",\n\t\t\tpatterns:      []string{\"test-web\", \"test-api\"},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard prefix match - excluded\",\n\t\t\tcontainerName: \"test-web\",\n\t\t\tpatterns:      []string{\"test-*\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard prefix match - not excluded\",\n\t\t\tcontainerName: \"prod-web\",\n\t\t\tpatterns:      []string{\"test-*\"},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard suffix match - excluded\",\n\t\t\tcontainerName: \"myapp-staging\",\n\t\t\tpatterns:      []string{\"*-staging\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard suffix match - not excluded\",\n\t\t\tcontainerName: \"myapp-prod\",\n\t\t\tpatterns:      []string{\"*-staging\"},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard both sides match - excluded\",\n\t\t\tcontainerName: \"test-myapp-staging\",\n\t\t\tpatterns:      []string{\"*-myapp-*\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard both sides match - not excluded\",\n\t\t\tcontainerName: \"prod-yourapp-live\",\n\t\t\tpatterns:      []string{\"*-myapp-*\"},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple patterns - matches first\",\n\t\t\tcontainerName: \"test-container\",\n\t\t\tpatterns:      []string{\"test-*\", \"*-staging\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple patterns - matches second\",\n\t\t\tcontainerName: \"myapp-staging\",\n\t\t\tpatterns:      []string{\"test-*\", \"*-staging\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple patterns - no match\",\n\t\t\tcontainerName: \"prod-web\",\n\t\t\tpatterns:      []string{\"test-*\", \"*-staging\"},\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"mixed exact and wildcard - exact match\",\n\t\t\tcontainerName: \"temp-container\",\n\t\t\tpatterns:      []string{\"temp-container\", \"test-*\"},\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"mixed exact and wildcard - wildcard match\",\n\t\t\tcontainerName: \"test-web\",\n\t\t\tpatterns:      []string{\"temp-container\", \"test-*\"},\n\t\t\texpected:      true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdm := &dockerManager{\n\t\t\t\texcludeContainers: tt.patterns,\n\t\t\t}\n\t\t\tresult := dm.shouldExcludeContainer(tt.containerName)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestAnsiEscapePattern(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no ANSI codes\",\n\t\t\tinput:    \"Hello, World!\",\n\t\t\texpected: \"Hello, World!\",\n\t\t},\n\t\t{\n\t\t\tname:     \"simple color code\",\n\t\t\tinput:    \"\\x1b[34mINFO\\x1b[0m client mode\",\n\t\t\texpected: \"INFO client mode\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple color codes\",\n\t\t\tinput:    \"\\x1b[31mERROR\\x1b[0m: \\x1b[33mWarning\\x1b[0m message\",\n\t\t\texpected: \"ERROR: Warning message\",\n\t\t},\n\t\t{\n\t\t\tname:     \"bold and color\",\n\t\t\tinput:    \"\\x1b[1;32mSUCCESS\\x1b[0m\",\n\t\t\texpected: \"SUCCESS\",\n\t\t},\n\t\t{\n\t\t\tname:     \"cursor movement codes\",\n\t\t\tinput:    \"Line 1\\x1b[KLine 2\",\n\t\t\texpected: \"Line 1Line 2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"256 color code\",\n\t\t\tinput:    \"\\x1b[38;5;196mRed text\\x1b[0m\",\n\t\t\texpected: \"Red text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"RGB/truecolor code\",\n\t\t\tinput:    \"\\x1b[38;2;255;0;0mRed text\\x1b[0m\",\n\t\t\texpected: \"Red text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"mixed content with newlines\",\n\t\t\tinput:    \"\\x1b[34m2024-01-01 12:00:00\\x1b[0m INFO Starting\\n\\x1b[31m2024-01-01 12:00:01\\x1b[0m ERROR Failed\",\n\t\t\texpected: \"2024-01-01 12:00:00 INFO Starting\\n2024-01-01 12:00:01 ERROR Failed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := ansiEscapePattern.ReplaceAllString(tt.input, \"\")\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestConvertContainerPortsToString(t *testing.T) {\n\ttype port = struct {\n\t\tPublicPort uint16\n\t\tIP         string\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\tports    []port\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty ports\",\n\t\t\tports:    nil,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"single port\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t},\n\t\t\texpected: \"80\",\n\t\t},\n\t\t{\n\t\t\tname: \"single port with non-default IP\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 80, IP: \"1.2.3.4\"},\n\t\t\t},\n\t\t\texpected: \"1.2.3.4:80\",\n\t\t},\n\t\t{\n\t\t\tname: \"ipv6 default ip\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 80, IP: \"::\"},\n\t\t\t},\n\t\t\texpected: \"80\",\n\t\t},\n\t\t{\n\t\t\tname: \"zero PublicPort is skipped\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 0, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t},\n\t\t\texpected: \"80\",\n\t\t},\n\t\t{\n\t\t\tname: \"ports sorted ascending by PublicPort\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 443, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 8080, IP: \"0.0.0.0\"},\n\t\t\t},\n\t\t\texpected: \"80, 443, 8080\",\n\t\t},\n\t\t{\n\t\t\tname: \"duplicates are deduplicated\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 443, IP: \"0.0.0.0\"},\n\t\t\t},\n\t\t\texpected: \"80, 443\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple ports with different IPs\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 80, IP: \"0.0.0.0\"},\n\t\t\t\t{PublicPort: 443, IP: \"1.2.3.4\"},\n\t\t\t},\n\t\t\texpected: \"80, 1.2.3.4:443\",\n\t\t},\n\t\t{\n\t\t\tname: \"ports slice is nilled after call\",\n\t\t\tports: []port{\n\t\t\t\t{PublicPort: 8080, IP: \"0.0.0.0\"},\n\t\t\t},\n\t\t\texpected: \"8080\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctr := &container.ApiInfo{}\n\t\t\tfor _, p := range tt.ports {\n\t\t\t\tctr.Ports = append(ctr.Ports, struct {\n\t\t\t\t\tPublicPort uint16\n\t\t\t\t\tIP         string\n\t\t\t\t}{PublicPort: p.PublicPort, IP: p.IP})\n\t\t\t}\n\t\t\tresult := convertContainerPortsToString(ctr)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\t// Ports slice must be cleared to prevent bleed-over into the next response\n\t\t\tassert.Nil(t, ctr.Ports, \"ctr.Ports should be nil after formatContainerPorts\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "agent/emmc_common.go",
    "content": "package agent\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc isEmmcBlockName(name string) bool {\n\tif !strings.HasPrefix(name, \"mmcblk\") {\n\t\treturn false\n\t}\n\tsuffix := strings.TrimPrefix(name, \"mmcblk\")\n\tif suffix == \"\" {\n\t\treturn false\n\t}\n\tfor _, c := range suffix {\n\t\tif c < '0' || c > '9' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc parseHexOrDecByte(s string) (uint8, bool) {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn 0, false\n\t}\n\tbase := 10\n\tif strings.HasPrefix(s, \"0x\") || strings.HasPrefix(s, \"0X\") {\n\t\tbase = 16\n\t\ts = s[2:]\n\t}\n\tparsed, err := strconv.ParseUint(s, base, 8)\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\treturn uint8(parsed), true\n}\n\nfunc parseHexBytePair(s string) (uint8, uint8, bool) {\n\tfields := strings.Fields(s)\n\tif len(fields) < 2 {\n\t\treturn 0, 0, false\n\t}\n\ta, okA := parseHexOrDecByte(fields[0])\n\tb, okB := parseHexOrDecByte(fields[1])\n\tif !okA && !okB {\n\t\treturn 0, 0, false\n\t}\n\treturn a, b, true\n}\n\nfunc emmcSmartStatus(preEOL uint8) string {\n\tswitch preEOL {\n\tcase 0x01:\n\t\treturn \"PASSED\"\n\tcase 0x02:\n\t\treturn \"WARNING\"\n\tcase 0x03:\n\t\treturn \"FAILED\"\n\tdefault:\n\t\treturn \"UNKNOWN\"\n\t}\n}\n\nfunc emmcPreEOLString(preEOL uint8) string {\n\tswitch preEOL {\n\tcase 0x01:\n\t\treturn \"0x01 (normal)\"\n\tcase 0x02:\n\t\treturn \"0x02 (warning)\"\n\tcase 0x03:\n\t\treturn \"0x03 (urgent)\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"0x%02x\", preEOL)\n\t}\n}\n\nfunc emmcLifeTimeString(v uint8) string {\n\t// JEDEC eMMC: 0x01..0x0A => 0-100% used in 10% steps, 0x0B => exceeded.\n\tswitch {\n\tcase v == 0:\n\t\treturn \"0x00 (not reported)\"\n\tcase v >= 0x01 && v <= 0x0A:\n\t\tlow := int(v-1) * 10\n\t\thigh := int(v) * 10\n\t\treturn fmt.Sprintf(\"0x%02x (%d-%d%% used)\", v, low, high)\n\tcase v == 0x0B:\n\t\treturn \"0x0b (>100% used)\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"0x%02x\", v)\n\t}\n}\n"
  },
  {
    "path": "agent/emmc_common_test.go",
    "content": "package agent\n\nimport \"testing\"\n\nfunc TestParseHexOrDecByte(t *testing.T) {\n\ttests := []struct {\n\t\tin   string\n\t\twant uint8\n\t\tok   bool\n\t}{\n\t\t{\"0x01\", 1, true},\n\t\t{\"0X0b\", 11, true},\n\t\t{\"01\", 1, true},\n\t\t{\" 3 \", 3, true},\n\t\t{\"\", 0, false},\n\t\t{\"0x\", 0, false},\n\t\t{\"nope\", 0, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tgot, ok := parseHexOrDecByte(tt.in)\n\t\tif ok != tt.ok || got != tt.want {\n\t\t\tt.Fatalf(\"parseHexOrDecByte(%q) = (%d,%v), want (%d,%v)\", tt.in, got, ok, tt.want, tt.ok)\n\t\t}\n\t}\n}\n\nfunc TestParseHexBytePair(t *testing.T) {\n\ta, b, ok := parseHexBytePair(\"0x01 0x02\\n\")\n\tif !ok || a != 1 || b != 2 {\n\t\tt.Fatalf(\"parseHexBytePair hex = (%d,%d,%v), want (1,2,true)\", a, b, ok)\n\t}\n\n\ta, b, ok = parseHexBytePair(\"01 02\")\n\tif !ok || a != 1 || b != 2 {\n\t\tt.Fatalf(\"parseHexBytePair dec = (%d,%d,%v), want (1,2,true)\", a, b, ok)\n\t}\n\n\t_, _, ok = parseHexBytePair(\"0x01\")\n\tif ok {\n\t\tt.Fatalf(\"parseHexBytePair short input ok=true, want false\")\n\t}\n}\n\nfunc TestEmmcSmartStatus(t *testing.T) {\n\tif got := emmcSmartStatus(0x01); got != \"PASSED\" {\n\t\tt.Fatalf(\"emmcSmartStatus(0x01) = %q, want PASSED\", got)\n\t}\n\tif got := emmcSmartStatus(0x02); got != \"WARNING\" {\n\t\tt.Fatalf(\"emmcSmartStatus(0x02) = %q, want WARNING\", got)\n\t}\n\tif got := emmcSmartStatus(0x03); got != \"FAILED\" {\n\t\tt.Fatalf(\"emmcSmartStatus(0x03) = %q, want FAILED\", got)\n\t}\n\tif got := emmcSmartStatus(0x00); got != \"UNKNOWN\" {\n\t\tt.Fatalf(\"emmcSmartStatus(0x00) = %q, want UNKNOWN\", got)\n\t}\n}\n\nfunc TestIsEmmcBlockName(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tok   bool\n\t}{\n\t\t{\"mmcblk0\", true},\n\t\t{\"mmcblk1\", true},\n\t\t{\"mmcblk10\", true},\n\t\t{\"mmcblk0p1\", false},\n\t\t{\"sda\", false},\n\t\t{\"mmcblk\", false},\n\t\t{\"mmcblkA\", false},\n\t}\n\tfor _, c := range cases {\n\t\tif got := isEmmcBlockName(c.name); got != c.ok {\n\t\t\tt.Fatalf(\"isEmmcBlockName(%q) = %v, want %v\", c.name, got, c.ok)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agent/emmc_linux.go",
    "content": "//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/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n)\n\n// emmcSysfsRoot is a test hook; production value is \"/sys\".\nvar emmcSysfsRoot = \"/sys\"\n\ntype emmcHealth struct {\n\tmodel    string\n\tserial   string\n\trevision string\n\tcapacity uint64\n\tpreEOL   uint8\n\tlifeA    uint8\n\tlifeB    uint8\n}\n\nfunc scanEmmcDevices() []*DeviceInfo {\n\tblockDir := filepath.Join(emmcSysfsRoot, \"class\", \"block\")\n\tentries, err := os.ReadDir(blockDir)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tdevices := make([]*DeviceInfo, 0, 2)\n\tfor _, ent := range entries {\n\t\tname := ent.Name()\n\t\tif !isEmmcBlockName(name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdeviceDir := filepath.Join(blockDir, name, \"device\")\n\t\tif !hasEmmcHealthFiles(deviceDir) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdevPath := filepath.Join(\"/dev\", name)\n\t\tdevices = append(devices, &DeviceInfo{\n\t\t\tName:     devPath,\n\t\t\tType:     \"emmc\",\n\t\t\tInfoName: devPath + \" [eMMC]\",\n\t\t\tProtocol: \"MMC\",\n\t\t})\n\t}\n\n\treturn devices\n}\n\nfunc (sm *SmartManager) collectEmmcHealth(deviceInfo *DeviceInfo) (bool, error) {\n\tif deviceInfo == nil || deviceInfo.Name == \"\" {\n\t\treturn false, nil\n\t}\n\n\tbase := filepath.Base(deviceInfo.Name)\n\tif !isEmmcBlockName(base) && !strings.EqualFold(deviceInfo.Type, \"emmc\") && !strings.EqualFold(deviceInfo.Type, \"mmc\") {\n\t\treturn false, nil\n\t}\n\n\thealth, ok := readEmmcHealth(base)\n\tif !ok {\n\t\treturn false, nil\n\t}\n\n\t// Normalize the device type to keep pruning logic stable across refreshes.\n\tdeviceInfo.Type = \"emmc\"\n\n\tkey := health.serial\n\tif key == \"\" {\n\t\tkey = filepath.Join(\"/dev\", base)\n\t}\n\n\tstatus := emmcSmartStatus(health.preEOL)\n\n\tattrs := []*smart.SmartAttribute{\n\t\t{\n\t\t\tName:      \"PreEOLInfo\",\n\t\t\tRawValue:  uint64(health.preEOL),\n\t\t\tRawString: emmcPreEOLString(health.preEOL),\n\t\t},\n\t\t{\n\t\t\tName:      \"DeviceLifeTimeEstA\",\n\t\t\tRawValue:  uint64(health.lifeA),\n\t\t\tRawString: emmcLifeTimeString(health.lifeA),\n\t\t},\n\t\t{\n\t\t\tName:      \"DeviceLifeTimeEstB\",\n\t\t\tRawValue:  uint64(health.lifeB),\n\t\t\tRawString: emmcLifeTimeString(health.lifeB),\n\t\t},\n\t}\n\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tif _, exists := sm.SmartDataMap[key]; !exists {\n\t\tsm.SmartDataMap[key] = &smart.SmartData{}\n\t}\n\n\tdata := sm.SmartDataMap[key]\n\tdata.ModelName = health.model\n\tdata.SerialNumber = health.serial\n\tdata.FirmwareVersion = health.revision\n\tdata.Capacity = health.capacity\n\tdata.Temperature = 0\n\tdata.SmartStatus = status\n\tdata.DiskName = filepath.Join(\"/dev\", base)\n\tdata.DiskType = \"emmc\"\n\tdata.Attributes = attrs\n\n\treturn true, nil\n}\n\nfunc readEmmcHealth(blockName string) (emmcHealth, bool) {\n\tvar out emmcHealth\n\n\tif !isEmmcBlockName(blockName) {\n\t\treturn out, false\n\t}\n\n\tdeviceDir := filepath.Join(emmcSysfsRoot, \"class\", \"block\", blockName, \"device\")\n\tpreEOL, okPre := readHexByteFile(filepath.Join(deviceDir, \"pre_eol_info\"))\n\n\t// Some kernels expose EXT_CSD lifetime via \"life_time\" (two bytes), others as\n\t// separate files. Support both.\n\tlifeA, lifeB, okLife := readLifeTime(deviceDir)\n\n\tif !okPre && !okLife {\n\t\treturn out, false\n\t}\n\n\tout.preEOL = preEOL\n\tout.lifeA = lifeA\n\tout.lifeB = lifeB\n\n\tout.model = utils.ReadStringFile(filepath.Join(deviceDir, \"name\"))\n\tout.serial = utils.ReadStringFile(filepath.Join(deviceDir, \"serial\"))\n\tout.revision = utils.ReadStringFile(filepath.Join(deviceDir, \"prv\"))\n\n\tif capBytes, ok := readBlockCapacityBytes(blockName); ok {\n\t\tout.capacity = capBytes\n\t}\n\n\treturn out, true\n}\n\nfunc readLifeTime(deviceDir string) (uint8, uint8, bool) {\n\tif content, ok := utils.ReadStringFileOK(filepath.Join(deviceDir, \"life_time\")); ok {\n\t\ta, b, ok := parseHexBytePair(content)\n\t\treturn a, b, ok\n\t}\n\n\ta, okA := readHexByteFile(filepath.Join(deviceDir, \"device_life_time_est_typ_a\"))\n\tb, okB := readHexByteFile(filepath.Join(deviceDir, \"device_life_time_est_typ_b\"))\n\tif okA || okB {\n\t\treturn a, b, true\n\t}\n\treturn 0, 0, false\n}\n\nfunc readBlockCapacityBytes(blockName string) (uint64, bool) {\n\tsizePath := filepath.Join(emmcSysfsRoot, \"class\", \"block\", blockName, \"size\")\n\tlbsPath := filepath.Join(emmcSysfsRoot, \"class\", \"block\", blockName, \"queue\", \"logical_block_size\")\n\n\tsizeStr, ok := utils.ReadStringFileOK(sizePath)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tsectors, err := strconv.ParseUint(sizeStr, 10, 64)\n\tif err != nil || sectors == 0 {\n\t\treturn 0, false\n\t}\n\n\tlbsStr, ok := utils.ReadStringFileOK(lbsPath)\n\tlogicalBlockSize := uint64(512)\n\tif ok {\n\t\tif parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {\n\t\t\tlogicalBlockSize = parsed\n\t\t}\n\t}\n\n\treturn sectors * logicalBlockSize, true\n}\n\nfunc readHexByteFile(path string) (uint8, bool) {\n\tcontent, ok := utils.ReadStringFileOK(path)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tb, ok := parseHexOrDecByte(content)\n\treturn b, ok\n}\n\nfunc hasEmmcHealthFiles(deviceDir string) bool {\n\tentries, err := os.ReadDir(deviceDir)\n\tif err != nil {\n\t\treturn false\n\t}\n\tfor _, ent := range entries {\n\t\tswitch ent.Name() {\n\t\tcase \"pre_eol_info\", \"life_time\", \"device_life_time_est_typ_a\", \"device_life_time_est_typ_b\":\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "agent/emmc_linux_test.go",
    "content": "//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/entities/smart\"\n)\n\nfunc TestEmmcMockSysfsScanAndCollect(t *testing.T) {\n\ttmp := t.TempDir()\n\tprev := emmcSysfsRoot\n\temmcSysfsRoot = tmp\n\tt.Cleanup(func() { emmcSysfsRoot = prev })\n\n\t// Fake: /sys/class/block/mmcblk0\n\tmmcDeviceDir := filepath.Join(tmp, \"class\", \"block\", \"mmcblk0\", \"device\")\n\tmmcQueueDir := filepath.Join(tmp, \"class\", \"block\", \"mmcblk0\", \"queue\")\n\tif err := os.MkdirAll(mmcDeviceDir, 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.MkdirAll(mmcQueueDir, 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twrite := func(path, content string) {\n\t\tt.Helper()\n\t\tif err := os.WriteFile(path, []byte(content), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\twrite(filepath.Join(mmcDeviceDir, \"pre_eol_info\"), \"0x02\\n\")\n\twrite(filepath.Join(mmcDeviceDir, \"life_time\"), \"0x04 0x05\\n\")\n\twrite(filepath.Join(mmcDeviceDir, \"name\"), \"H26M52103FMR\\n\")\n\twrite(filepath.Join(mmcDeviceDir, \"serial\"), \"01234567\\n\")\n\twrite(filepath.Join(mmcDeviceDir, \"prv\"), \"0x08\\n\")\n\twrite(filepath.Join(mmcQueueDir, \"logical_block_size\"), \"512\\n\")\n\twrite(filepath.Join(tmp, \"class\", \"block\", \"mmcblk0\", \"size\"), \"1024\\n\") // sectors\n\n\tdevs := scanEmmcDevices()\n\tif len(devs) != 1 {\n\t\tt.Fatalf(\"scanEmmcDevices() = %d devices, want 1\", len(devs))\n\t}\n\tif devs[0].Name != \"/dev/mmcblk0\" || devs[0].Type != \"emmc\" {\n\t\tt.Fatalf(\"scanEmmcDevices()[0] = %+v, want Name=/dev/mmcblk0 Type=emmc\", devs[0])\n\t}\n\n\tsm := &SmartManager{SmartDataMap: map[string]*smart.SmartData{}}\n\tok, err := sm.collectEmmcHealth(devs[0])\n\tif err != nil || !ok {\n\t\tt.Fatalf(\"collectEmmcHealth() = (ok=%v, err=%v), want (true,nil)\", ok, err)\n\t}\n\tif len(sm.SmartDataMap) != 1 {\n\t\tt.Fatalf(\"SmartDataMap len=%d, want 1\", len(sm.SmartDataMap))\n\t}\n\tvar got *smart.SmartData\n\tfor _, v := range sm.SmartDataMap {\n\t\tgot = v\n\t\tbreak\n\t}\n\tif got == nil {\n\t\tt.Fatalf(\"SmartDataMap value nil\")\n\t}\n\tif got.DiskType != \"emmc\" || got.DiskName != \"/dev/mmcblk0\" {\n\t\tt.Fatalf(\"disk fields = (type=%q name=%q), want (emmc,/dev/mmcblk0)\", got.DiskType, got.DiskName)\n\t}\n\tif got.SmartStatus != \"WARNING\" {\n\t\tt.Fatalf(\"SmartStatus=%q, want WARNING\", got.SmartStatus)\n\t}\n\tif got.SerialNumber != \"01234567\" || got.ModelName == \"\" || got.Capacity == 0 {\n\t\tt.Fatalf(\"identity fields = (model=%q serial=%q cap=%d), want non-empty model, serial 01234567, cap>0\", got.ModelName, got.SerialNumber, got.Capacity)\n\t}\n\tif len(got.Attributes) < 3 {\n\t\tt.Fatalf(\"attributes len=%d, want >= 3\", len(got.Attributes))\n\t}\n}\n"
  },
  {
    "path": "agent/emmc_stub.go",
    "content": "//go:build !linux\n\npackage agent\n\n// Non-Linux builds: eMMC health via sysfs is not available.\n\nfunc scanEmmcDevices() []*DeviceInfo {\n\treturn nil\n}\n\nfunc (sm *SmartManager) collectEmmcHealth(deviceInfo *DeviceInfo) (bool, error) {\n\treturn false, nil\n}\n\n"
  },
  {
    "path": "agent/fingerprint.go",
    "content": "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/shirou/gopsutil/v4/cpu\"\n\t\"github.com/shirou/gopsutil/v4/host\"\n)\n\nconst fingerprintFileName = \"fingerprint\"\n\n// knownBadUUID is a commonly known \"product_uuid\" that is not unique across systems.\nconst knownBadUUID = \"03000200-0400-0500-0006-000700080009\"\n\n// GetFingerprint returns the agent fingerprint. It first tries to read a saved\n// fingerprint from the data directory. If not found (or dataDir is empty), it\n// generates one from system properties. The hostname and cpuModel parameters are\n// used as fallback material if host.HostID() fails. If either is empty, they\n// are fetched from the system automatically.\n//\n// If a new fingerprint is generated and a dataDir is provided, it is saved.\nfunc GetFingerprint(dataDir, hostname, cpuModel string) string {\n\tif dataDir != \"\" {\n\t\tif fp, err := readFingerprint(dataDir); err == nil {\n\t\t\treturn fp\n\t\t}\n\t}\n\tfp := generateFingerprint(hostname, cpuModel)\n\tif dataDir != \"\" {\n\t\t_ = SaveFingerprint(dataDir, fp)\n\t}\n\treturn fp\n}\n\n// generateFingerprint creates a fingerprint from system properties.\n// It tries host.HostID() first, falling back to hostname + cpuModel.\n// If hostname or cpuModel are empty, they are fetched from the system.\nfunc generateFingerprint(hostname, cpuModel string) string {\n\tfingerprint, err := host.HostID()\n\tif err != nil || fingerprint == \"\" || fingerprint == knownBadUUID {\n\t\tif hostname == \"\" {\n\t\t\thostname, _ = os.Hostname()\n\t\t}\n\t\tif cpuModel == \"\" {\n\t\t\tif info, err := cpu.Info(); err == nil && len(info) > 0 {\n\t\t\t\tcpuModel = info[0].ModelName\n\t\t\t}\n\t\t}\n\t\tfingerprint = hostname + cpuModel\n\t}\n\n\tsum := sha256.Sum256([]byte(fingerprint))\n\treturn hex.EncodeToString(sum[:24])\n}\n\n// readFingerprint reads the saved fingerprint from the data directory.\nfunc readFingerprint(dataDir string) (string, error) {\n\tfp, err := os.ReadFile(filepath.Join(dataDir, fingerprintFileName))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ts := strings.TrimSpace(string(fp))\n\tif s == \"\" {\n\t\treturn \"\", errors.New(\"fingerprint file is empty\")\n\t}\n\treturn s, nil\n}\n\n// SaveFingerprint writes the fingerprint to the data directory.\nfunc SaveFingerprint(dataDir, fingerprint string) error {\n\treturn os.WriteFile(filepath.Join(dataDir, fingerprintFileName), []byte(fingerprint), 0o644)\n}\n\n// DeleteFingerprint removes the saved fingerprint file from the data directory.\n// Returns nil if the file does not exist (idempotent).\nfunc DeleteFingerprint(dataDir string) error {\n\terr := os.Remove(filepath.Join(dataDir, fingerprintFileName))\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn nil\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "agent/fingerprint_test.go",
    "content": "//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\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetFingerprint(t *testing.T) {\n\tt.Run(\"reads existing fingerprint from file\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\texpected := \"abc123def456\"\n\t\terr := os.WriteFile(filepath.Join(dir, fingerprintFileName), []byte(expected), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tfp := GetFingerprint(dir, \"\", \"\")\n\t\tassert.Equal(t, expected, fp)\n\t})\n\n\tt.Run(\"trims whitespace from file\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\terr := os.WriteFile(filepath.Join(dir, fingerprintFileName), []byte(\"  abc123  \\n\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tfp := GetFingerprint(dir, \"\", \"\")\n\t\tassert.Equal(t, \"abc123\", fp)\n\t})\n\n\tt.Run(\"generates fingerprint when file does not exist\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\tfp := GetFingerprint(dir, \"\", \"\")\n\t\tassert.NotEmpty(t, fp)\n\t})\n\n\tt.Run(\"generates fingerprint when dataDir is empty\", func(t *testing.T) {\n\t\tfp := GetFingerprint(\"\", \"\", \"\")\n\t\tassert.NotEmpty(t, fp)\n\t})\n\n\tt.Run(\"generates consistent fingerprint for same inputs\", func(t *testing.T) {\n\t\tfp1 := GetFingerprint(\"\", \"myhost\", \"mycpu\")\n\t\tfp2 := GetFingerprint(\"\", \"myhost\", \"mycpu\")\n\t\tassert.Equal(t, fp1, fp2)\n\t})\n\n\tt.Run(\"prefers saved fingerprint over generated\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\trequire.NoError(t, SaveFingerprint(dir, \"saved-fp\"))\n\n\t\tfp := GetFingerprint(dir, \"anyhost\", \"anycpu\")\n\t\tassert.Equal(t, \"saved-fp\", fp)\n\t})\n}\n\nfunc TestSaveFingerprint(t *testing.T) {\n\tt.Run(\"saves fingerprint to file\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\terr := SaveFingerprint(dir, \"abc123\")\n\t\trequire.NoError(t, err)\n\n\t\tcontent, err := os.ReadFile(filepath.Join(dir, fingerprintFileName))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"abc123\", string(content))\n\t})\n\n\tt.Run(\"overwrites existing fingerprint\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\trequire.NoError(t, SaveFingerprint(dir, \"old\"))\n\t\trequire.NoError(t, SaveFingerprint(dir, \"new\"))\n\n\t\tcontent, err := os.ReadFile(filepath.Join(dir, fingerprintFileName))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"new\", string(content))\n\t})\n}\n\nfunc TestDeleteFingerprint(t *testing.T) {\n\tt.Run(\"deletes existing fingerprint\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\tfp := filepath.Join(dir, fingerprintFileName)\n\t\terr := os.WriteFile(fp, []byte(\"abc123\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = DeleteFingerprint(dir)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify file is gone\n\t\t_, err = os.Stat(fp)\n\t\tassert.True(t, os.IsNotExist(err))\n\t})\n\n\tt.Run(\"no error when file does not exist\", func(t *testing.T) {\n\t\tdir := t.TempDir()\n\t\terr := DeleteFingerprint(dir)\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "agent/gpu.go",
    "content": "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\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nconst (\n\t// Commands\n\tnvidiaSmiCmd    string = \"nvidia-smi\"\n\trocmSmiCmd      string = \"rocm-smi\"\n\ttegraStatsCmd   string = \"tegrastats\"\n\tnvtopCmd        string = \"nvtop\"\n\tpowermetricsCmd string = \"powermetrics\"\n\tmacmonCmd       string = \"macmon\"\n\tnoGPUFoundMsg   string = \"no GPU found - see https://beszel.dev/guide/gpu\"\n\n\t// Command retry and timeout constants\n\tretryWaitTime     time.Duration = 5 * time.Second\n\tmaxFailureRetries int           = 5\n\n\t// Unit Conversions\n\tmebibytesInAMegabyte float64 = 1.024  // nvidia-smi reports memory in MiB\n\tmilliwattsInAWatt    float64 = 1000.0 // tegrastats reports power in mW\n)\n\n// GPUManager manages data collection for GPUs (either Nvidia or AMD)\ntype GPUManager struct {\n\tsync.Mutex\n\tGpuDataMap map[string]*system.GPUData\n\t// lastAvgData stores the last calculated averages for each GPU\n\t// Used when a collection happens before new data arrives (Count == 0)\n\tlastAvgData map[string]system.GPUData\n\t// Per-cache-key tracking for delta calculations\n\t// cacheKey -> gpuId -> snapshot of last count/usage/power values\n\tlastSnapshots map[uint16]map[string]*gpuSnapshot\n}\n\n// gpuSnapshot stores the last observed incremental values for delta tracking\ntype gpuSnapshot struct {\n\tcount    uint32\n\tusage    float64\n\tpower    float64\n\tpowerPkg float64\n\tengines  map[string]float64\n}\n\n// RocmSmiJson represents the JSON structure of rocm-smi output\ntype RocmSmiJson struct {\n\tID           string `json:\"GUID\"`\n\tName         string `json:\"Card series\"`\n\tTemperature  string `json:\"Temperature (Sensor edge) (C)\"`\n\tMemoryUsed   string `json:\"VRAM Total Used Memory (B)\"`\n\tMemoryTotal  string `json:\"VRAM Total Memory (B)\"`\n\tUsage        string `json:\"GPU use (%)\"`\n\tPowerPackage string `json:\"Average Graphics Package Power (W)\"`\n\tPowerSocket  string `json:\"Current Socket Graphics Package Power (W)\"`\n}\n\n// gpuCollector defines a collector for a specific GPU management utility (nvidia-smi or rocm-smi)\ntype gpuCollector struct {\n\tname    string\n\tcmdArgs []string\n\tparse   func([]byte) bool // returns true if valid data was found\n\tbuf     []byte\n\tbufSize uint16\n}\n\nvar errNoValidData = fmt.Errorf(\"no valid GPU data found\") // Error for missing data\n\n// collectorSource identifies a selectable GPU collector in GPU_COLLECTOR.\ntype collectorSource string\n\nconst (\n\tcollectorSourceNVTop        collectorSource = collectorSource(nvtopCmd)\n\tcollectorSourceNVML         collectorSource = \"nvml\"\n\tcollectorSourceNvidiaSMI    collectorSource = collectorSource(nvidiaSmiCmd)\n\tcollectorSourceIntelGpuTop  collectorSource = collectorSource(intelGpuStatsCmd)\n\tcollectorSourceAmdSysfs     collectorSource = \"amd_sysfs\"\n\tcollectorSourceRocmSMI      collectorSource = collectorSource(rocmSmiCmd)\n\tcollectorSourceMacmon       collectorSource = collectorSource(macmonCmd)\n\tcollectorSourcePowermetrics collectorSource = collectorSource(powermetricsCmd)\n\tcollectorGroupNvidia        string          = \"nvidia\"\n\tcollectorGroupIntel         string          = \"intel\"\n\tcollectorGroupAmd           string          = \"amd\"\n\tcollectorGroupApple         string          = \"apple\"\n)\n\nfunc isValidCollectorSource(source collectorSource) bool {\n\tswitch source {\n\tcase collectorSourceNVTop,\n\t\tcollectorSourceNVML,\n\t\tcollectorSourceNvidiaSMI,\n\t\tcollectorSourceIntelGpuTop,\n\t\tcollectorSourceAmdSysfs,\n\t\tcollectorSourceRocmSMI,\n\t\tcollectorSourceMacmon,\n\t\tcollectorSourcePowermetrics:\n\t\treturn true\n\t}\n\treturn false\n}\n\n// gpuCapabilities describes detected GPU tooling and sysfs support on the host.\ntype gpuCapabilities struct {\n\thasNvidiaSmi    bool\n\thasRocmSmi      bool\n\thasAmdSysfs     bool\n\thasTegrastats   bool\n\thasIntelGpuTop  bool\n\thasNvtop        bool\n\thasMacmon       bool\n\thasPowermetrics bool\n}\n\ntype collectorDefinition struct {\n\tgroup              string\n\tavailable          bool\n\tstart              func(onFailure func()) bool\n\tdeprecationWarning string\n}\n\n// starts and manages the ongoing collection of GPU data for the specified GPU management utility\nfunc (c *gpuCollector) start() {\n\tfor {\n\t\terr := c.collect()\n\t\tif err != nil {\n\t\t\tif err == errNoValidData {\n\t\t\t\tslog.Warn(c.name + \" found no valid GPU data, stopping\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tslog.Warn(c.name+\" failed, restarting\", \"err\", err)\n\t\t\ttime.Sleep(retryWaitTime)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// collect executes the command, parses output with the assigned parser function\nfunc (c *gpuCollector) collect() error {\n\tcmd := exec.Command(c.name, c.cmdArgs...)\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\tscanner := bufio.NewScanner(stdout)\n\tif c.buf == nil {\n\t\tc.buf = make([]byte, 0, c.bufSize)\n\t}\n\tscanner.Buffer(c.buf, bufio.MaxScanTokenSize)\n\n\tfor scanner.Scan() {\n\t\thasValidData := c.parse(scanner.Bytes())\n\t\tif !hasValidData {\n\t\t\treturn errNoValidData\n\t\t}\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn fmt.Errorf(\"scanner error: %w\", err)\n\t}\n\treturn cmd.Wait()\n}\n\n// getJetsonParser returns a function to parse the output of tegrastats and update the GPUData map\nfunc (gm *GPUManager) getJetsonParser() func(output []byte) bool {\n\t// use closure to avoid recompiling the regex\n\tramPattern := regexp.MustCompile(`RAM (\\d+)/(\\d+)MB`)\n\tgr3dPattern := regexp.MustCompile(`GR3D_FREQ (\\d+)%`)\n\ttempPattern := regexp.MustCompile(`(?:tj|GPU)@(\\d+\\.?\\d*)C`)\n\t// Orin Nano / NX do not have GPU specific power monitor\n\t// TODO: Maybe use VDD_IN for Nano / NX and add a total system power chart\n\tpowerPattern := regexp.MustCompile(`(GPU_SOC|CPU_GPU_CV)\\s+(\\d+)mW|VDD_SYS_GPU\\s+(\\d+)/\\d+`)\n\n\t// jetson devices have only one gpu so we'll just initialize here\n\tgpuData := &system.GPUData{Name: \"GPU\"}\n\tgm.GpuDataMap[\"0\"] = gpuData\n\n\treturn func(output []byte) bool {\n\t\tgm.Lock()\n\t\tdefer gm.Unlock()\n\t\t// Parse RAM usage\n\t\tramMatches := ramPattern.FindSubmatch(output)\n\t\tif ramMatches != nil {\n\t\t\tgpuData.MemoryUsed, _ = strconv.ParseFloat(string(ramMatches[1]), 64)\n\t\t\tgpuData.MemoryTotal, _ = strconv.ParseFloat(string(ramMatches[2]), 64)\n\t\t}\n\t\t// Parse GR3D (GPU) usage\n\t\tgr3dMatches := gr3dPattern.FindSubmatch(output)\n\t\tif gr3dMatches != nil {\n\t\t\tgr3dUsage, _ := strconv.ParseFloat(string(gr3dMatches[1]), 64)\n\t\t\tgpuData.Usage += gr3dUsage\n\t\t}\n\t\t// Parse temperature\n\t\ttempMatches := tempPattern.FindSubmatch(output)\n\t\tif tempMatches != nil {\n\t\t\tgpuData.Temperature, _ = strconv.ParseFloat(string(tempMatches[1]), 64)\n\t\t}\n\t\t// Parse power usage\n\t\tpowerMatches := powerPattern.FindSubmatch(output)\n\t\tif powerMatches != nil {\n\t\t\t// powerMatches[2] is the \"(GPU_SOC|CPU_GPU_CV) <N>mW\" capture\n\t\t\t// powerMatches[3] is the \"VDD_SYS_GPU <N>/<N>\" capture\n\t\t\tpowerStr := string(powerMatches[2])\n\t\t\tif powerStr == \"\" {\n\t\t\t\tpowerStr = string(powerMatches[3])\n\t\t\t}\n\t\t\tpower, _ := strconv.ParseFloat(powerStr, 64)\n\t\t\tgpuData.Power += power / milliwattsInAWatt\n\t\t}\n\t\tgpuData.Count++\n\t\treturn true\n\t}\n}\n\n// parseNvidiaData parses the output of nvidia-smi and updates the GPUData map\nfunc (gm *GPUManager) parseNvidiaData(output []byte) bool {\n\tgm.Lock()\n\tdefer gm.Unlock()\n\tscanner := bufio.NewScanner(bytes.NewReader(output))\n\tvar valid bool\n\tfor scanner.Scan() {\n\t\tline := scanner.Text() // Or use scanner.Bytes() for []byte\n\t\tfields := strings.Split(strings.TrimSpace(line), \", \")\n\t\tif len(fields) < 7 {\n\t\t\tcontinue\n\t\t}\n\t\tvalid = true\n\t\tid := fields[0]\n\t\ttemp, _ := strconv.ParseFloat(fields[2], 64)\n\t\tmemoryUsage, _ := strconv.ParseFloat(fields[3], 64)\n\t\ttotalMemory, _ := strconv.ParseFloat(fields[4], 64)\n\t\tusage, _ := strconv.ParseFloat(fields[5], 64)\n\t\tpower, _ := strconv.ParseFloat(fields[6], 64)\n\t\t// add gpu if not exists\n\t\tif _, ok := gm.GpuDataMap[id]; !ok {\n\t\t\tname := strings.TrimPrefix(fields[1], \"NVIDIA \")\n\t\t\tgm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, \" Laptop GPU\")}\n\t\t}\n\t\t// update gpu data\n\t\tgpu := gm.GpuDataMap[id]\n\t\tgpu.Temperature = temp\n\t\tgpu.MemoryUsed = memoryUsage / mebibytesInAMegabyte\n\t\tgpu.MemoryTotal = totalMemory / mebibytesInAMegabyte\n\t\tgpu.Usage += usage\n\t\tgpu.Power += power\n\t\tgpu.Count++\n\t}\n\treturn valid\n}\n\n// parseAmdData parses the output of rocm-smi and updates the GPUData map\nfunc (gm *GPUManager) parseAmdData(output []byte) bool {\n\tvar rocmSmiInfo map[string]RocmSmiJson\n\tif err := json.Unmarshal(output, &rocmSmiInfo); err != nil || len(rocmSmiInfo) == 0 {\n\t\treturn false\n\t}\n\tgm.Lock()\n\tdefer gm.Unlock()\n\tfor _, v := range rocmSmiInfo {\n\t\tvar power float64\n\t\tif v.PowerPackage != \"\" {\n\t\t\tpower, _ = strconv.ParseFloat(v.PowerPackage, 64)\n\t\t} else {\n\t\t\tpower, _ = strconv.ParseFloat(v.PowerSocket, 64)\n\t\t}\n\t\tmemoryUsage, _ := strconv.ParseFloat(v.MemoryUsed, 64)\n\t\ttotalMemory, _ := strconv.ParseFloat(v.MemoryTotal, 64)\n\t\tusage, _ := strconv.ParseFloat(v.Usage, 64)\n\n\t\tid := v.ID\n\t\tif _, ok := gm.GpuDataMap[id]; !ok {\n\t\t\tgm.GpuDataMap[id] = &system.GPUData{Name: v.Name}\n\t\t}\n\t\tgpu := gm.GpuDataMap[id]\n\t\tgpu.Temperature, _ = strconv.ParseFloat(v.Temperature, 64)\n\t\tgpu.MemoryUsed = utils.BytesToMegabytes(memoryUsage)\n\t\tgpu.MemoryTotal = utils.BytesToMegabytes(totalMemory)\n\t\tgpu.Usage += usage\n\t\tgpu.Power += power\n\t\tgpu.Count++\n\t}\n\treturn true\n}\n\n// GetCurrentData returns GPU utilization data averaged since the last call with this cacheKey\nfunc (gm *GPUManager) GetCurrentData(cacheKey uint16) map[string]system.GPUData {\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\tgm.initializeSnapshots(cacheKey)\n\tnameCounts := gm.countGPUNames()\n\n\tgpuData := make(map[string]system.GPUData, len(gm.GpuDataMap))\n\tfor id, gpu := range gm.GpuDataMap {\n\t\tgpuAvg := gm.calculateGPUAverage(id, gpu, cacheKey)\n\t\tgm.updateInstantaneousValues(&gpuAvg, gpu)\n\t\tgm.storeSnapshot(id, gpu, cacheKey)\n\n\t\t// Append id to name if there are multiple GPUs with the same name\n\t\tif nameCounts[gpu.Name] > 1 {\n\t\t\tgpuAvg.Name = fmt.Sprintf(\"%s %s\", gpu.Name, id)\n\t\t}\n\t\tgpuData[id] = gpuAvg\n\t}\n\tslog.Debug(\"GPU\", \"data\", gpuData)\n\treturn gpuData\n}\n\n// initializeSnapshots ensures snapshot maps are initialized for the given cache key\nfunc (gm *GPUManager) initializeSnapshots(cacheKey uint16) {\n\tif gm.lastAvgData == nil {\n\t\tgm.lastAvgData = make(map[string]system.GPUData)\n\t}\n\tif gm.lastSnapshots == nil {\n\t\tgm.lastSnapshots = make(map[uint16]map[string]*gpuSnapshot)\n\t}\n\tif gm.lastSnapshots[cacheKey] == nil {\n\t\tgm.lastSnapshots[cacheKey] = make(map[string]*gpuSnapshot)\n\t}\n}\n\n// countGPUNames returns a map of GPU names to their occurrence count\nfunc (gm *GPUManager) countGPUNames() map[string]int {\n\tnameCounts := make(map[string]int)\n\tfor _, gpu := range gm.GpuDataMap {\n\t\tnameCounts[gpu.Name]++\n\t}\n\treturn nameCounts\n}\n\n// calculateGPUAverage computes the average GPU metrics since the last snapshot for this cache key\nfunc (gm *GPUManager) calculateGPUAverage(id string, gpu *system.GPUData, cacheKey uint16) system.GPUData {\n\tlastSnapshot := gm.lastSnapshots[cacheKey][id]\n\tcurrentCount := uint32(gpu.Count)\n\tdeltaCount := gm.calculateDeltaCount(currentCount, lastSnapshot)\n\n\t// If no new data arrived\n\tif deltaCount == 0 {\n\t\t// If GPU appears suspended (instantaneous values are 0), return zero values\n\t\t// Otherwise return last known average for temporary collection gaps\n\t\tif gpu.Temperature == 0 && gpu.MemoryUsed == 0 {\n\t\t\treturn system.GPUData{Name: gpu.Name}\n\t\t}\n\t\treturn gm.lastAvgData[id] // zero value if not found\n\t}\n\n\t// Calculate new average\n\tgpuAvg := *gpu\n\tdeltaUsage, deltaPower, deltaPowerPkg := gm.calculateDeltas(gpu, lastSnapshot)\n\n\tgpuAvg.Power = utils.TwoDecimals(deltaPower / float64(deltaCount))\n\n\tif gpu.Engines != nil {\n\t\t// make fresh map for averaged engine metrics to avoid mutating\n\t\t// the accumulator map stored in gm.GpuDataMap\n\t\tgpuAvg.Engines = make(map[string]float64, len(gpu.Engines))\n\t\tgpuAvg.Usage = gm.calculateIntelGPUUsage(&gpuAvg, gpu, lastSnapshot, deltaCount)\n\t\tgpuAvg.PowerPkg = utils.TwoDecimals(deltaPowerPkg / float64(deltaCount))\n\t} else {\n\t\tgpuAvg.Usage = utils.TwoDecimals(deltaUsage / float64(deltaCount))\n\t}\n\n\tgm.lastAvgData[id] = gpuAvg\n\treturn gpuAvg\n}\n\n// calculateDeltaCount returns the change in count since the last snapshot\nfunc (gm *GPUManager) calculateDeltaCount(currentCount uint32, lastSnapshot *gpuSnapshot) uint32 {\n\tif lastSnapshot != nil {\n\t\treturn currentCount - lastSnapshot.count\n\t}\n\treturn currentCount\n}\n\n// calculateDeltas computes the change in usage, power, and powerPkg since the last snapshot\nfunc (gm *GPUManager) calculateDeltas(gpu *system.GPUData, lastSnapshot *gpuSnapshot) (deltaUsage, deltaPower, deltaPowerPkg float64) {\n\tif lastSnapshot != nil {\n\t\treturn gpu.Usage - lastSnapshot.usage,\n\t\t\tgpu.Power - lastSnapshot.power,\n\t\t\tgpu.PowerPkg - lastSnapshot.powerPkg\n\t}\n\treturn gpu.Usage, gpu.Power, gpu.PowerPkg\n}\n\n// calculateIntelGPUUsage computes Intel GPU usage from engine metrics and returns max engine usage\nfunc (gm *GPUManager) calculateIntelGPUUsage(gpuAvg, gpu *system.GPUData, lastSnapshot *gpuSnapshot, deltaCount uint32) float64 {\n\tmaxEngineUsage := 0.0\n\tfor name, engine := range gpu.Engines {\n\t\tvar deltaEngine float64\n\t\tif lastSnapshot != nil && lastSnapshot.engines != nil {\n\t\t\tdeltaEngine = engine - lastSnapshot.engines[name]\n\t\t} else {\n\t\t\tdeltaEngine = engine\n\t\t}\n\t\tgpuAvg.Engines[name] = utils.TwoDecimals(deltaEngine / float64(deltaCount))\n\t\tmaxEngineUsage = max(maxEngineUsage, deltaEngine/float64(deltaCount))\n\t}\n\treturn utils.TwoDecimals(maxEngineUsage)\n}\n\n// updateInstantaneousValues updates values that should reflect current state, not averages\nfunc (gm *GPUManager) updateInstantaneousValues(gpuAvg *system.GPUData, gpu *system.GPUData) {\n\tgpuAvg.Temperature = utils.TwoDecimals(gpu.Temperature)\n\tgpuAvg.MemoryUsed = utils.TwoDecimals(gpu.MemoryUsed)\n\tgpuAvg.MemoryTotal = utils.TwoDecimals(gpu.MemoryTotal)\n}\n\n// storeSnapshot saves the current GPU state for this cache key\nfunc (gm *GPUManager) storeSnapshot(id string, gpu *system.GPUData, cacheKey uint16) {\n\tsnapshot := &gpuSnapshot{\n\t\tcount:    uint32(gpu.Count),\n\t\tusage:    gpu.Usage,\n\t\tpower:    gpu.Power,\n\t\tpowerPkg: gpu.PowerPkg,\n\t}\n\tif gpu.Engines != nil {\n\t\tsnapshot.engines = make(map[string]float64, len(gpu.Engines))\n\t\tmaps.Copy(snapshot.engines, gpu.Engines)\n\t}\n\tgm.lastSnapshots[cacheKey][id] = snapshot\n}\n\n// discoverGpuCapabilities checks for available GPU tooling and sysfs support.\n// It only reports capability presence and does not apply policy decisions.\nfunc (gm *GPUManager) discoverGpuCapabilities() gpuCapabilities {\n\tcaps := gpuCapabilities{\n\t\thasAmdSysfs: gm.hasAmdSysfs(),\n\t}\n\tif _, err := exec.LookPath(nvidiaSmiCmd); err == nil {\n\t\tcaps.hasNvidiaSmi = true\n\t}\n\tif _, err := exec.LookPath(rocmSmiCmd); err == nil {\n\t\tcaps.hasRocmSmi = true\n\t}\n\tif _, err := exec.LookPath(tegraStatsCmd); err == nil {\n\t\tcaps.hasTegrastats = true\n\t}\n\tif _, err := exec.LookPath(intelGpuStatsCmd); err == nil {\n\t\tcaps.hasIntelGpuTop = true\n\t}\n\tif _, err := exec.LookPath(nvtopCmd); err == nil {\n\t\tcaps.hasNvtop = true\n\t}\n\tif runtime.GOOS == \"darwin\" {\n\t\tif _, err := exec.LookPath(macmonCmd); err == nil {\n\t\t\tcaps.hasMacmon = true\n\t\t}\n\t\tif _, err := exec.LookPath(powermetricsCmd); err == nil {\n\t\t\tcaps.hasPowermetrics = true\n\t\t}\n\t}\n\treturn caps\n}\n\nfunc hasAnyGpuCollector(caps gpuCapabilities) bool {\n\treturn caps.hasNvidiaSmi || caps.hasRocmSmi || caps.hasAmdSysfs || caps.hasTegrastats || caps.hasIntelGpuTop || caps.hasNvtop || caps.hasMacmon || caps.hasPowermetrics\n}\n\nfunc (gm *GPUManager) startIntelCollector() {\n\tgo func() {\n\t\tfailures := 0\n\t\tfor {\n\t\t\tif err := gm.collectIntelStats(); err != nil {\n\t\t\t\tfailures++\n\t\t\t\tif failures > maxFailureRetries {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Error collecting Intel GPU data; see https://beszel.dev/guide/gpu\", \"err\", err)\n\t\t\t\ttime.Sleep(retryWaitTime)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc (gm *GPUManager) startNvidiaSmiCollector(intervalSeconds string) {\n\tcollector := gpuCollector{\n\t\tname:    nvidiaSmiCmd,\n\t\tbufSize: 10 * 1024,\n\t\tcmdArgs: []string{\n\t\t\t\"-l\", intervalSeconds,\n\t\t\t\"--query-gpu=index,name,temperature.gpu,memory.used,memory.total,utilization.gpu,power.draw\",\n\t\t\t\"--format=csv,noheader,nounits\",\n\t\t},\n\t\tparse: gm.parseNvidiaData,\n\t}\n\tgo collector.start()\n}\n\nfunc (gm *GPUManager) startTegraStatsCollector(intervalMilliseconds string) {\n\tcollector := gpuCollector{\n\t\tname:    tegraStatsCmd,\n\t\tbufSize: 10 * 1024,\n\t\tcmdArgs: []string{\"--interval\", intervalMilliseconds},\n\t\tparse:   gm.getJetsonParser(),\n\t}\n\tgo collector.start()\n}\n\nfunc (gm *GPUManager) startRocmSmiCollector(pollInterval time.Duration) {\n\tcollector := gpuCollector{\n\t\tname:    rocmSmiCmd,\n\t\tbufSize: 10 * 1024,\n\t\tcmdArgs: []string{\"--showid\", \"--showtemp\", \"--showuse\", \"--showpower\", \"--showproductname\", \"--showmeminfo\", \"vram\", \"--json\"},\n\t\tparse:   gm.parseAmdData,\n\t}\n\tgo func() {\n\t\tfailures := 0\n\t\tfor {\n\t\t\tif err := collector.collect(); err != nil {\n\t\t\t\tfailures++\n\t\t\t\tif failures > maxFailureRetries {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Error collecting AMD GPU data via rocm-smi\", \"err\", err)\n\t\t\t}\n\t\t\ttime.Sleep(pollInterval)\n\t\t}\n\t}()\n}\n\nfunc (gm *GPUManager) collectorDefinitions(caps gpuCapabilities) map[collectorSource]collectorDefinition {\n\treturn map[collectorSource]collectorDefinition{\n\t\tcollectorSourceNVML: {\n\t\t\tgroup:     collectorGroupNvidia,\n\t\t\tavailable: caps.hasNvidiaSmi,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\treturn gm.startNvmlCollector()\n\t\t\t},\n\t\t},\n\t\tcollectorSourceNvidiaSMI: {\n\t\t\tgroup:     collectorGroupNvidia,\n\t\t\tavailable: caps.hasNvidiaSmi,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\tgm.startNvidiaSmiCollector(\"4\") // seconds\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t\tcollectorSourceIntelGpuTop: {\n\t\t\tgroup:     collectorGroupIntel,\n\t\t\tavailable: caps.hasIntelGpuTop,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\tgm.startIntelCollector()\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t\tcollectorSourceAmdSysfs: {\n\t\t\tgroup:     collectorGroupAmd,\n\t\t\tavailable: caps.hasAmdSysfs,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\treturn gm.startAmdSysfsCollector()\n\t\t\t},\n\t\t},\n\t\tcollectorSourceRocmSMI: {\n\t\t\tgroup:              collectorGroupAmd,\n\t\t\tavailable:          caps.hasRocmSmi,\n\t\t\tdeprecationWarning: \"rocm-smi is deprecated and may be removed in a future release\",\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\tgm.startRocmSmiCollector(4300 * time.Millisecond)\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t\tcollectorSourceNVTop: {\n\t\t\tavailable: caps.hasNvtop,\n\t\t\tstart: func(onFailure func()) bool {\n\t\t\t\tgm.startNvtopCollector(\"30\", onFailure) // tens of milliseconds\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t\tcollectorSourceMacmon: {\n\t\t\tgroup:     collectorGroupApple,\n\t\t\tavailable: caps.hasMacmon,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\tgm.startMacmonCollector()\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t\tcollectorSourcePowermetrics: {\n\t\t\tgroup:     collectorGroupApple,\n\t\t\tavailable: caps.hasPowermetrics,\n\t\t\tstart: func(_ func()) bool {\n\t\t\t\tgm.startPowermetricsCollector()\n\t\t\t\treturn true\n\t\t\t},\n\t\t},\n\t}\n}\n\n// parseCollectorPriority parses GPU_COLLECTOR and returns valid ordered entries.\nfunc parseCollectorPriority(value string) []collectorSource {\n\tparts := strings.Split(value, \",\")\n\tpriorities := make([]collectorSource, 0, len(parts))\n\tfor _, raw := range parts {\n\t\tname := collectorSource(strings.TrimSpace(strings.ToLower(raw)))\n\t\tif !isValidCollectorSource(name) {\n\t\t\tif name != \"\" {\n\t\t\t\tslog.Warn(\"Ignoring unknown GPU collector\", \"collector\", name)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tpriorities = append(priorities, name)\n\t}\n\treturn priorities\n}\n\n// startNvmlCollector initializes NVML and starts its polling loop.\nfunc (gm *GPUManager) startNvmlCollector() bool {\n\tcollector := &nvmlCollector{gm: gm}\n\tif err := collector.init(); err != nil {\n\t\tslog.Warn(\"Failed to initialize NVML\", \"err\", err)\n\t\treturn false\n\t}\n\tgo collector.start()\n\treturn true\n}\n\n// startAmdSysfsCollector starts AMD GPU collection via sysfs.\nfunc (gm *GPUManager) startAmdSysfsCollector() bool {\n\tgo func() {\n\t\tif err := gm.collectAmdStats(); err != nil {\n\t\t\tslog.Warn(\"Error collecting AMD GPU data via sysfs\", \"err\", err)\n\t\t}\n\t}()\n\treturn true\n}\n\n// startCollectorsByPriority starts collectors in order with one source per vendor group.\nfunc (gm *GPUManager) startCollectorsByPriority(priorities []collectorSource, caps gpuCapabilities) int {\n\tdefinitions := gm.collectorDefinitions(caps)\n\tselectedGroups := make(map[string]bool, 3)\n\tstarted := 0\n\tfor i, source := range priorities {\n\t\tdefinition, ok := definitions[source]\n\t\tif !ok || !definition.available {\n\t\t\tcontinue\n\t\t}\n\t\t// nvtop is not a vendor-specific collector, so should only be used if no other collectors are selected or it is first in GPU_COLLECTOR.\n\t\tif source == collectorSourceNVTop {\n\t\t\tif len(selectedGroups) > 0 {\n\t\t\t\tslog.Warn(\"Skipping nvtop because other collectors are selected\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if nvtop fails, fall back to remaining collectors.\n\t\t\tremaining := append([]collectorSource(nil), priorities[i+1:]...)\n\t\t\tif definition.start(func() {\n\t\t\t\tgm.startCollectorsByPriority(remaining, caps)\n\t\t\t}) {\n\t\t\t\tstarted++\n\t\t\t\treturn started\n\t\t\t}\n\t\t}\n\t\tgroup := definition.group\n\t\tif group == \"\" || selectedGroups[group] {\n\t\t\tcontinue\n\t\t}\n\t\tif definition.deprecationWarning != \"\" {\n\t\t\tslog.Warn(definition.deprecationWarning)\n\t\t}\n\t\tif definition.start(nil) {\n\t\t\tselectedGroups[group] = true\n\t\t\tstarted++\n\t\t}\n\t}\n\treturn started\n}\n\n// resolveLegacyCollectorPriority builds the default collector order when GPU_COLLECTOR is unset.\nfunc (gm *GPUManager) resolveLegacyCollectorPriority(caps gpuCapabilities) []collectorSource {\n\tpriorities := make([]collectorSource, 0, 4)\n\n\tif caps.hasNvidiaSmi && !caps.hasTegrastats {\n\t\tif nvml, _ := utils.GetEnv(\"NVML\"); nvml == \"true\" {\n\t\t\tpriorities = append(priorities, collectorSourceNVML, collectorSourceNvidiaSMI)\n\t\t} else {\n\t\t\tpriorities = append(priorities, collectorSourceNvidiaSMI)\n\t\t}\n\t}\n\n\tif caps.hasRocmSmi {\n\t\tif val, _ := utils.GetEnv(\"AMD_SYSFS\"); val == \"true\" {\n\t\t\tpriorities = append(priorities, collectorSourceAmdSysfs)\n\t\t} else {\n\t\t\tpriorities = append(priorities, collectorSourceRocmSMI)\n\t\t}\n\t} else if caps.hasAmdSysfs {\n\t\tpriorities = append(priorities, collectorSourceAmdSysfs)\n\t}\n\n\tif caps.hasIntelGpuTop {\n\t\tpriorities = append(priorities, collectorSourceIntelGpuTop)\n\t}\n\n\t// Apple collectors are currently opt-in only for testing.\n\t// Enable them with GPU_COLLECTOR=macmon or GPU_COLLECTOR=powermetrics.\n\t// TODO: uncomment below when Apple collectors are confirmed to be working.\n\t//\n\t// Prefer macmon on macOS (no sudo). Fall back to powermetrics if present.\n\t// if caps.hasMacmon {\n\t// \tpriorities = append(priorities, collectorSourceMacmon)\n\t// } else if caps.hasPowermetrics {\n\t// \tpriorities = append(priorities, collectorSourcePowermetrics)\n\t// }\n\n\t// Keep nvtop as a last resort only when no vendor collector exists.\n\tif len(priorities) == 0 && caps.hasNvtop {\n\t\tpriorities = append(priorities, collectorSourceNVTop)\n\t}\n\treturn priorities\n}\n\n// NewGPUManager creates and initializes a new GPUManager\nfunc NewGPUManager() (*GPUManager, error) {\n\tif skipGPU, _ := utils.GetEnv(\"SKIP_GPU\"); skipGPU == \"true\" {\n\t\treturn nil, nil\n\t}\n\tvar gm GPUManager\n\tcaps := gm.discoverGpuCapabilities()\n\tif !hasAnyGpuCollector(caps) {\n\t\treturn nil, fmt.Errorf(noGPUFoundMsg)\n\t}\n\tgm.GpuDataMap = make(map[string]*system.GPUData)\n\n\t// Jetson devices should always use tegrastats (ignore GPU_COLLECTOR).\n\tif caps.hasTegrastats {\n\t\tgm.startTegraStatsCollector(\"3700\")\n\t\treturn &gm, nil\n\t}\n\n\t// if GPU_COLLECTOR is set, start user-defined collectors.\n\tif collectorConfig, ok := utils.GetEnv(\"GPU_COLLECTOR\"); ok && strings.TrimSpace(collectorConfig) != \"\" {\n\t\tpriorities := parseCollectorPriority(collectorConfig)\n\t\tif gm.startCollectorsByPriority(priorities, caps) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"no configured GPU collectors are available\")\n\t\t}\n\t\treturn &gm, nil\n\t}\n\n\t// auto-detect and start collectors when GPU_COLLECTOR is unset.\n\tif gm.startCollectorsByPriority(gm.resolveLegacyCollectorPriority(caps), caps) == 0 {\n\t\treturn nil, fmt.Errorf(noGPUFoundMsg)\n\t}\n\n\treturn &gm, nil\n}\n"
  },
  {
    "path": "agent/gpu_amd_linux.go",
    "content": "//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\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nvar amdgpuNameCache = struct {\n\tsync.RWMutex\n\thits   map[string]string\n\tmisses map[string]struct{}\n}{\n\thits:   make(map[string]string),\n\tmisses: make(map[string]struct{}),\n}\n\n// hasAmdSysfs returns true if any AMD GPU sysfs nodes are found\nfunc (gm *GPUManager) hasAmdSysfs() bool {\n\tcards, err := filepath.Glob(\"/sys/class/drm/card*/device/vendor\")\n\tif err != nil {\n\t\treturn false\n\t}\n\tfor _, vendorPath := range cards {\n\t\tvendor, err := utils.ReadStringFileLimited(vendorPath, 64)\n\t\tif err == nil && vendor == \"0x1002\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// collectAmdStats collects AMD GPU metrics directly from sysfs to avoid the overhead of rocm-smi\nfunc (gm *GPUManager) collectAmdStats() error {\n\tsysfsPollInterval := 3000 * time.Millisecond\n\tcards, err := filepath.Glob(\"/sys/class/drm/card*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar amdGpuPaths []string\n\tfor _, card := range cards {\n\t\t// Ignore symbolic links and non-main card directories\n\t\tif strings.Contains(filepath.Base(card), \"-\") || !isAmdGpu(card) {\n\t\t\tcontinue\n\t\t}\n\t\tamdGpuPaths = append(amdGpuPaths, card)\n\t}\n\n\tif len(amdGpuPaths) == 0 {\n\t\treturn errNoValidData\n\t}\n\n\tslog.Debug(\"Using sysfs for AMD GPU data collection\")\n\n\tfailures := 0\n\tfor {\n\t\thasData := false\n\t\tfor _, cardPath := range amdGpuPaths {\n\t\t\tif gm.updateAmdGpuData(cardPath) {\n\t\t\t\thasData = true\n\t\t\t}\n\t\t}\n\t\tif !hasData {\n\t\t\tfailures++\n\t\t\tif failures > maxFailureRetries {\n\t\t\t\treturn errNoValidData\n\t\t\t}\n\t\t\tslog.Warn(\"No AMD GPU data from sysfs\", \"failures\", failures)\n\t\t\ttime.Sleep(retryWaitTime)\n\t\t\tcontinue\n\t\t}\n\t\tfailures = 0\n\t\ttime.Sleep(sysfsPollInterval)\n\t}\n}\n\n// isAmdGpu checks whether a DRM card path belongs to AMD vendor ID 0x1002.\nfunc isAmdGpu(cardPath string) bool {\n\tvendor, err := utils.ReadStringFileLimited(filepath.Join(cardPath, \"device/vendor\"), 64)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn vendor == \"0x1002\"\n}\n\n// updateAmdGpuData reads GPU metrics from sysfs and updates the GPU data map.\n// Returns true if at least some data was successfully read.\nfunc (gm *GPUManager) updateAmdGpuData(cardPath string) bool {\n\tdevicePath := filepath.Join(cardPath, \"device\")\n\tid := filepath.Base(cardPath)\n\n\t// Read all sysfs values first (no lock needed - these can be slow)\n\tusage, usageErr := readSysfsFloat(filepath.Join(devicePath, \"gpu_busy_percent\"))\n\tmemUsed, memUsedErr := readSysfsFloat(filepath.Join(devicePath, \"mem_info_vram_used\"))\n\tmemTotal, _ := readSysfsFloat(filepath.Join(devicePath, \"mem_info_vram_total\"))\n\t// if gtt is present, add it to the memory used and total (https://github.com/henrygd/beszel/issues/1569#issuecomment-3837640484)\n\tif gttUsed, err := readSysfsFloat(filepath.Join(devicePath, \"mem_info_gtt_used\")); err == nil && gttUsed > 0 {\n\t\tif gttTotal, err := readSysfsFloat(filepath.Join(devicePath, \"mem_info_gtt_total\")); err == nil {\n\t\t\tmemUsed += gttUsed\n\t\t\tmemTotal += gttTotal\n\t\t}\n\t}\n\n\tvar temp, power float64\n\thwmons, _ := filepath.Glob(filepath.Join(devicePath, \"hwmon/hwmon*\"))\n\tfor _, hwmonDir := range hwmons {\n\t\tif t, err := readSysfsFloat(filepath.Join(hwmonDir, \"temp1_input\")); err == nil {\n\t\t\ttemp = t / 1000.0\n\t\t}\n\t\tif p, err := readSysfsFloat(filepath.Join(hwmonDir, \"power1_average\")); err == nil {\n\t\t\tpower += p / 1000000.0\n\t\t} else if p, err := readSysfsFloat(filepath.Join(hwmonDir, \"power1_input\")); err == nil {\n\t\t\tpower += p / 1000000.0\n\t\t}\n\t}\n\n\t// Check if we got any meaningful data\n\tif usageErr != nil && memUsedErr != nil && temp == 0 {\n\t\treturn false\n\t}\n\n\t// Single lock to update all values atomically\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\tgpu, ok := gm.GpuDataMap[id]\n\tif !ok {\n\t\tgpu = &system.GPUData{Name: getAmdGpuName(devicePath)}\n\t\tgm.GpuDataMap[id] = gpu\n\t}\n\n\tif usageErr == nil {\n\t\tgpu.Usage += usage\n\t}\n\tgpu.MemoryUsed = utils.BytesToMegabytes(memUsed)\n\tgpu.MemoryTotal = utils.BytesToMegabytes(memTotal)\n\tgpu.Temperature = temp\n\tgpu.Power += power\n\tgpu.Count++\n\treturn true\n}\n\n// readSysfsFloat reads and parses a numeric value from a sysfs file.\nfunc readSysfsFloat(path string) (float64, error) {\n\tval, err := utils.ReadStringFileLimited(path, 64)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn strconv.ParseFloat(val, 64)\n}\n\n// normalizeHexID normalizes hex IDs by trimming spaces, lowercasing, and dropping 0x.\nfunc normalizeHexID(id string) string {\n\treturn strings.TrimPrefix(strings.ToLower(strings.TrimSpace(id)), \"0x\")\n}\n\n// cacheKeyForAmdgpu builds the cache key for a device and optional revision.\nfunc cacheKeyForAmdgpu(deviceID, revisionID string) string {\n\tif revisionID != \"\" {\n\t\treturn deviceID + \":\" + revisionID\n\t}\n\treturn deviceID\n}\n\n// lookupAmdgpuNameInFile resolves an AMDGPU name from amdgpu.ids by device/revision.\nfunc lookupAmdgpuNameInFile(deviceID, revisionID, filePath string) (name string, exact bool, found bool) {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn \"\", false, false\n\t}\n\tdefer file.Close()\n\n\tvar byDevice string\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif line == \"\" || strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\t\tparts := strings.SplitN(line, \",\", 3)\n\t\tif len(parts) != 3 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdev := normalizeHexID(parts[0])\n\t\trev := normalizeHexID(parts[1])\n\t\tproductName := strings.TrimSpace(parts[2])\n\t\tif dev == \"\" || productName == \"\" || dev != deviceID {\n\t\t\tcontinue\n\t\t}\n\t\tif byDevice == \"\" {\n\t\t\tbyDevice = productName\n\t\t}\n\t\tif revisionID != \"\" && rev == revisionID {\n\t\t\treturn productName, true, true\n\t\t}\n\t}\n\tif byDevice != \"\" {\n\t\treturn byDevice, false, true\n\t}\n\treturn \"\", false, false\n}\n\n// getCachedAmdgpuName returns cached hit/miss status for the given device/revision.\nfunc getCachedAmdgpuName(deviceID, revisionID string) (name string, found bool, done bool) {\n\t// Build the list of cache keys to check. We always look up the exact device+revision key.\n\t// When revisionID is set, we also look up deviceID alone, since the cache may store a\n\t// device-only fallback when we couldn't resolve the exact revision.\n\tkeys := []string{cacheKeyForAmdgpu(deviceID, revisionID)}\n\tif revisionID != \"\" {\n\t\tkeys = append(keys, deviceID)\n\t}\n\n\tknownMisses := 0\n\tamdgpuNameCache.RLock()\n\tdefer amdgpuNameCache.RUnlock()\n\tfor _, key := range keys {\n\t\tif name, ok := amdgpuNameCache.hits[key]; ok {\n\t\t\treturn name, true, true\n\t\t}\n\t\tif _, ok := amdgpuNameCache.misses[key]; ok {\n\t\t\tknownMisses++\n\t\t}\n\t}\n\t// done=true means \"don't bother doing slow lookup\": we either found a name (above) or\n\t// every key we checked was already a known miss, so we've tried before and failed.\n\treturn \"\", false, knownMisses == len(keys)\n}\n\n// normalizeAmdgpuName trims standard suffixes from AMDGPU product names.\nfunc normalizeAmdgpuName(name string) string {\n\tfor _, suffix := range []string{\" Graphics\", \" Series\"} {\n\t\tname = strings.TrimSuffix(name, suffix)\n\t}\n\treturn name\n}\n\n// cacheAmdgpuName stores a resolved AMDGPU name in the lookup cache.\nfunc cacheAmdgpuName(deviceID, revisionID, name string, exact bool) {\n\tname = normalizeAmdgpuName(name)\n\tamdgpuNameCache.Lock()\n\tdefer amdgpuNameCache.Unlock()\n\tif exact && revisionID != \"\" {\n\t\tamdgpuNameCache.hits[cacheKeyForAmdgpu(deviceID, revisionID)] = name\n\t}\n\tamdgpuNameCache.hits[deviceID] = name\n}\n\n// cacheMissingAmdgpuName records unresolved device/revision lookups.\nfunc cacheMissingAmdgpuName(deviceID, revisionID string) {\n\tamdgpuNameCache.Lock()\n\tdefer amdgpuNameCache.Unlock()\n\tamdgpuNameCache.misses[deviceID] = struct{}{}\n\tif revisionID != \"\" {\n\t\tamdgpuNameCache.misses[cacheKeyForAmdgpu(deviceID, revisionID)] = struct{}{}\n\t}\n}\n\n// getAmdGpuName attempts to get a descriptive GPU name.\n// First tries product_name (rarely available), then looks up the PCI device ID.\n// Falls back to showing the raw device ID if not found in the lookup table.\nfunc getAmdGpuName(devicePath string) string {\n\t// Try product_name first (works for some enterprise GPUs)\n\tif prod, err := utils.ReadStringFileLimited(filepath.Join(devicePath, \"product_name\"), 128); err == nil {\n\t\treturn prod\n\t}\n\n\t// Read PCI device ID and look it up\n\tif deviceID, err := utils.ReadStringFileLimited(filepath.Join(devicePath, \"device\"), 64); err == nil {\n\t\tid := normalizeHexID(deviceID)\n\t\trevision := \"\"\n\t\tif rev, revErr := utils.ReadStringFileLimited(filepath.Join(devicePath, \"revision\"), 64); revErr == nil {\n\t\t\trevision = normalizeHexID(rev)\n\t\t}\n\n\t\tif name, found, done := getCachedAmdgpuName(id, revision); found {\n\t\t\treturn name\n\t\t} else if !done {\n\t\t\tif name, exact, ok := lookupAmdgpuNameInFile(id, revision, \"/usr/share/libdrm/amdgpu.ids\"); ok {\n\t\t\t\tcacheAmdgpuName(id, revision, name, exact)\n\t\t\t\treturn normalizeAmdgpuName(name)\n\t\t\t}\n\t\t\tcacheMissingAmdgpuName(id, revision)\n\t\t}\n\n\t\treturn fmt.Sprintf(\"AMD GPU (%s)\", id)\n\t}\n\n\treturn \"AMD GPU\"\n}\n"
  },
  {
    "path": "agent/gpu_amd_linux_test.go",
    "content": "//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\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNormalizeHexID(t *testing.T) {\n\ttests := []struct {\n\t\tin   string\n\t\twant string\n\t}{\n\t\t{\"0x1002\", \"1002\"},\n\t\t{\"C2\", \"c2\"},\n\t\t{\"  15BF  \", \"15bf\"},\n\t\t{\"0x15bf\", \"15bf\"},\n\t\t{\"\", \"\"},\n\t}\n\tfor _, tt := range tests {\n\t\tsubName := tt.in\n\t\tif subName == \"\" {\n\t\t\tsubName = \"empty_string\"\n\t\t}\n\t\tt.Run(subName, func(t *testing.T) {\n\t\t\tgot := normalizeHexID(tt.in)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestCacheKeyForAmdgpu(t *testing.T) {\n\ttests := []struct {\n\t\tdeviceID   string\n\t\trevisionID string\n\t\twant       string\n\t}{\n\t\t{\"1114\", \"c2\", \"1114:c2\"},\n\t\t{\"15bf\", \"\", \"15bf\"},\n\t\t{\"1506\", \"c1\", \"1506:c1\"},\n\t}\n\tfor _, tt := range tests {\n\t\tgot := cacheKeyForAmdgpu(tt.deviceID, tt.revisionID)\n\t\tassert.Equal(t, tt.want, got)\n\t}\n}\n\nfunc TestReadSysfsFloat(t *testing.T) {\n\tdir := t.TempDir()\n\n\tvalidPath := filepath.Join(dir, \"val\")\n\trequire.NoError(t, os.WriteFile(validPath, []byte(\"  42.5  \\n\"), 0o644))\n\tgot, err := readSysfsFloat(validPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 42.5, got)\n\n\t// Integer and scientific\n\tsciPath := filepath.Join(dir, \"sci\")\n\trequire.NoError(t, os.WriteFile(sciPath, []byte(\"1e2\"), 0o644))\n\tgot, err = readSysfsFloat(sciPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 100.0, got)\n\n\t// Missing file\n\t_, err = readSysfsFloat(filepath.Join(dir, \"missing\"))\n\trequire.Error(t, err)\n\n\t// Invalid content\n\tbadPath := filepath.Join(dir, \"bad\")\n\trequire.NoError(t, os.WriteFile(badPath, []byte(\"not a number\"), 0o644))\n\t_, err = readSysfsFloat(badPath)\n\trequire.Error(t, err)\n}\n\nfunc TestIsAmdGpu(t *testing.T) {\n\tdir := t.TempDir()\n\tdeviceDir := filepath.Join(dir, \"device\")\n\trequire.NoError(t, os.MkdirAll(deviceDir, 0o755))\n\n\t// AMD vendor 0x1002 -> true\n\trequire.NoError(t, os.WriteFile(filepath.Join(deviceDir, \"vendor\"), []byte(\"0x1002\\n\"), 0o644))\n\tassert.True(t, isAmdGpu(dir), \"vendor 0x1002 should be AMD\")\n\n\t// Non-AMD vendor -> false\n\trequire.NoError(t, os.WriteFile(filepath.Join(deviceDir, \"vendor\"), []byte(\"0x10de\\n\"), 0o644))\n\tassert.False(t, isAmdGpu(dir), \"vendor 0x10de should not be AMD\")\n\n\t// Missing vendor file -> false\n\trequire.NoError(t, os.Remove(filepath.Join(deviceDir, \"vendor\")))\n\tassert.False(t, isAmdGpu(dir), \"missing vendor file should be false\")\n}\n\nfunc TestAmdgpuNameCacheRoundTrip(t *testing.T) {\n\t// Cache a name and retrieve it (unique key to avoid affecting other tests)\n\tdeviceID, revisionID := \"cachedev99\", \"00\"\n\tcacheAmdgpuName(deviceID, revisionID, \"AMD Test GPU 99 Graphics\", true)\n\n\tname, found, done := getCachedAmdgpuName(deviceID, revisionID)\n\tassert.True(t, found)\n\tassert.True(t, done)\n\tassert.Equal(t, \"AMD Test GPU 99\", name)\n\n\t// Device-only key also stored\n\tname2, found2, _ := getCachedAmdgpuName(deviceID, \"\")\n\tassert.True(t, found2)\n\tassert.Equal(t, \"AMD Test GPU 99\", name2)\n\n\t// Cache a miss\n\tcacheMissingAmdgpuName(\"missedev99\", \"ab\")\n\t_, found3, done3 := getCachedAmdgpuName(\"missedev99\", \"ab\")\n\tassert.False(t, found3)\n\tassert.True(t, done3, \"done should be true so caller skips file lookup\")\n}\n\nfunc TestUpdateAmdGpuDataWithFakeSysfs(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\twriteGTT        bool\n\t\twantMemoryUsed  float64\n\t\twantMemoryTotal float64\n\t}{\n\t\t{\n\t\t\tname:            \"sums vram and gtt when gtt is present\",\n\t\t\twriteGTT:        true,\n\t\t\twantMemoryUsed:  utils.BytesToMegabytes(1073741824 + 536870912),\n\t\t\twantMemoryTotal: utils.BytesToMegabytes(2147483648 + 4294967296),\n\t\t},\n\t\t{\n\t\t\tname:            \"falls back to vram when gtt is missing\",\n\t\t\twriteGTT:        false,\n\t\t\twantMemoryUsed:  utils.BytesToMegabytes(1073741824),\n\t\t\twantMemoryTotal: utils.BytesToMegabytes(2147483648),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdir := t.TempDir()\n\t\t\tcardPath := filepath.Join(dir, \"card0\")\n\t\t\tdevicePath := filepath.Join(cardPath, \"device\")\n\t\t\thwmonPath := filepath.Join(devicePath, \"hwmon\", \"hwmon0\")\n\t\t\trequire.NoError(t, os.MkdirAll(hwmonPath, 0o755))\n\n\t\t\twrite := func(name, content string) {\n\t\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(devicePath, name), []byte(content), 0o644))\n\t\t\t}\n\t\t\twrite(\"vendor\", \"0x1002\")\n\t\t\twrite(\"device\", \"0x1506\")\n\t\t\twrite(\"revision\", \"0xc1\")\n\t\t\twrite(\"gpu_busy_percent\", \"25\")\n\t\t\twrite(\"mem_info_vram_used\", \"1073741824\")\n\t\t\twrite(\"mem_info_vram_total\", \"2147483648\")\n\t\t\tif tt.writeGTT {\n\t\t\t\twrite(\"mem_info_gtt_used\", \"536870912\")\n\t\t\t\twrite(\"mem_info_gtt_total\", \"4294967296\")\n\t\t\t}\n\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(hwmonPath, \"temp1_input\"), []byte(\"45000\"), 0o644))\n\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(hwmonPath, \"power1_input\"), []byte(\"20000000\"), 0o644))\n\n\t\t\t// Pre-cache name so getAmdGpuName returns a known value (it uses system amdgpu.ids path)\n\t\t\tcacheAmdgpuName(\"1506\", \"c1\", \"AMD Radeon 610M Graphics\", true)\n\n\t\t\tgm := &GPUManager{GpuDataMap: make(map[string]*system.GPUData)}\n\t\t\tok := gm.updateAmdGpuData(cardPath)\n\t\t\trequire.True(t, ok)\n\n\t\t\tgpu, ok := gm.GpuDataMap[\"card0\"]\n\t\t\trequire.True(t, ok)\n\t\t\tassert.Equal(t, \"AMD Radeon 610M\", gpu.Name)\n\t\t\tassert.Equal(t, 25.0, gpu.Usage)\n\t\t\tassert.Equal(t, tt.wantMemoryUsed, gpu.MemoryUsed)\n\t\t\tassert.Equal(t, tt.wantMemoryTotal, gpu.MemoryTotal)\n\t\t\tassert.Equal(t, 45.0, gpu.Temperature)\n\t\t\tassert.Equal(t, 20.0, gpu.Power)\n\t\t\tassert.Equal(t, 1.0, gpu.Count)\n\t\t})\n\t}\n}\n\nfunc TestLookupAmdgpuNameInFile(t *testing.T) {\n\tidsPath := filepath.Join(\"test-data\", \"amdgpu.ids\")\n\n\ttests := []struct {\n\t\tname       string\n\t\tdeviceID   string\n\t\trevisionID string\n\t\twantName   string\n\t\twantExact  bool\n\t\twantFound  bool\n\t}{\n\t\t{\n\t\t\tname:       \"exact device and revision match\",\n\t\t\tdeviceID:   \"1114\",\n\t\t\trevisionID: \"c2\",\n\t\t\twantName:   \"AMD Radeon 860M Graphics\",\n\t\t\twantExact:  true,\n\t\t\twantFound:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"exact match 15BF revision 01 returns 760M\",\n\t\t\tdeviceID:   \"15bf\",\n\t\t\trevisionID: \"01\",\n\t\t\twantName:   \"AMD Radeon 760M Graphics\",\n\t\t\twantExact:  true,\n\t\t\twantFound:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"exact match 15BF revision 00 returns 780M\",\n\t\t\tdeviceID:   \"15bf\",\n\t\t\trevisionID: \"00\",\n\t\t\twantName:   \"AMD Radeon 780M Graphics\",\n\t\t\twantExact:  true,\n\t\t\twantFound:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"device-only match returns first entry for device\",\n\t\t\tdeviceID:   \"1506\",\n\t\t\trevisionID: \"\",\n\t\t\twantName:   \"AMD Radeon 610M\",\n\t\t\twantExact:  false,\n\t\t\twantFound:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"unknown device not found\",\n\t\t\tdeviceID:   \"dead\",\n\t\t\trevisionID: \"00\",\n\t\t\twantName:   \"\",\n\t\t\twantExact:  false,\n\t\t\twantFound:  false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotName, gotExact, gotFound := lookupAmdgpuNameInFile(tt.deviceID, tt.revisionID, idsPath)\n\t\t\tassert.Equal(t, tt.wantName, gotName, \"name\")\n\t\t\tassert.Equal(t, tt.wantExact, gotExact, \"exact\")\n\t\t\tassert.Equal(t, tt.wantFound, gotFound, \"found\")\n\t\t})\n\t}\n}\n\nfunc TestGetAmdGpuNameFromIdsFile(t *testing.T) {\n\t// Test that getAmdGpuName resolves a name when we can't inject the ids path.\n\t// We only verify behavior when product_name is missing and device/revision\n\t// would be read from sysfs; the actual lookup uses /usr/share/libdrm/amdgpu.ids.\n\t// So this test focuses on normalizeAmdgpuName and that lookupAmdgpuNameInFile\n\t// returns the expected name for our test-data file.\n\tidsPath := filepath.Join(\"test-data\", \"amdgpu.ids\")\n\tname, exact, found := lookupAmdgpuNameInFile(\"1435\", \"ae\", idsPath)\n\trequire.True(t, found)\n\trequire.True(t, exact)\n\tassert.Equal(t, \"AMD Custom GPU 0932\", name)\n\tassert.Equal(t, \"AMD Custom GPU 0932\", normalizeAmdgpuName(name))\n\n\t// \" Graphics\" suffix is trimmed by normalizeAmdgpuName\n\tname2 := \"AMD Radeon 860M Graphics\"\n\tassert.Equal(t, \"AMD Radeon 860M\", normalizeAmdgpuName(name2))\n}\n"
  },
  {
    "path": "agent/gpu_amd_unsupported.go",
    "content": "//go:build !linux\n\npackage agent\n\nimport (\n\t\"errors\"\n)\n\nfunc (gm *GPUManager) hasAmdSysfs() bool {\n\treturn false\n}\n\nfunc (gm *GPUManager) collectAmdStats() error {\n\treturn errors.ErrUnsupported\n}\n"
  },
  {
    "path": "agent/gpu_darwin.go",
    "content": "//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\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nconst (\n\t// powermetricsSampleIntervalMs is the sampling interval passed to powermetrics (-i).\n\tpowermetricsSampleIntervalMs = 500\n\t// powermetricsPollInterval is how often we run powermetrics to collect a new sample.\n\tpowermetricsPollInterval = 2 * time.Second\n\t// macmonIntervalMs is the sampling interval passed to macmon pipe (-i), in milliseconds.\n\tmacmonIntervalMs = 2500\n)\n\nconst appleGPUID = \"0\"\n\n// startPowermetricsCollector runs powermetrics --samplers gpu_power in a loop and updates\n// GPU usage and power. Requires root (sudo) on macOS. A single logical GPU is reported as id \"0\".\nfunc (gm *GPUManager) startPowermetricsCollector() {\n\t// Ensure single GPU entry for Apple GPU\n\tif _, ok := gm.GpuDataMap[appleGPUID]; !ok {\n\t\tgm.GpuDataMap[appleGPUID] = &system.GPUData{Name: \"Apple GPU\"}\n\t}\n\n\tgo func() {\n\t\tfailures := 0\n\t\tfor {\n\t\t\tif err := gm.collectPowermetrics(); err != nil {\n\t\t\t\tfailures++\n\t\t\t\tif failures > maxFailureRetries {\n\t\t\t\t\tslog.Warn(\"powermetrics GPU collector failed repeatedly, stopping\", \"err\", err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Error collecting macOS GPU data via powermetrics (may require sudo)\", \"err\", err)\n\t\t\t\ttime.Sleep(retryWaitTime)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfailures = 0\n\t\t\ttime.Sleep(powermetricsPollInterval)\n\t\t}\n\t}()\n}\n\n// collectPowermetrics runs powermetrics once and parses GPU usage and power from its output.\nfunc (gm *GPUManager) collectPowermetrics() error {\n\tinterval := strconv.Itoa(powermetricsSampleIntervalMs)\n\tcmd := exec.Command(powermetricsCmd, \"--samplers\", \"gpu_power\", \"-i\", interval, \"-n\", \"1\")\n\tcmd.Stderr = nil\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !gm.parsePowermetricsData(out) {\n\t\treturn errNoValidData\n\t}\n\treturn nil\n}\n\n// parsePowermetricsData parses powermetrics gpu_power output and updates GpuDataMap[\"0\"].\n// Example output:\n//\n//\t**** GPU usage ****\n//\tGPU HW active frequency: 444 MHz\n//\tGPU HW active residency:   0.97% (444 MHz: .97% ...\n//\tGPU idle residency:  99.03%\n//\tGPU Power: 4 mW\nfunc (gm *GPUManager) parsePowermetricsData(output []byte) bool {\n\tvar idleResidency, powerMW float64\n\tvar gotIdle, gotPower bool\n\n\tscanner := bufio.NewScanner(bytes.NewReader(output))\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif strings.HasPrefix(line, \"GPU idle residency:\") {\n\t\t\t// \"GPU idle residency:  99.03%\"\n\t\t\tfields := strings.Fields(strings.TrimPrefix(line, \"GPU idle residency:\"))\n\t\t\tif len(fields) >= 1 {\n\t\t\t\tpct := strings.TrimSuffix(fields[0], \"%\")\n\t\t\t\tif v, err := strconv.ParseFloat(pct, 64); err == nil {\n\t\t\t\t\tidleResidency = v\n\t\t\t\t\tgotIdle = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else if strings.HasPrefix(line, \"GPU Power:\") {\n\t\t\t// \"GPU Power: 4 mW\"\n\t\t\tfields := strings.Fields(strings.TrimPrefix(line, \"GPU Power:\"))\n\t\t\tif len(fields) >= 1 {\n\t\t\t\tif v, err := strconv.ParseFloat(fields[0], 64); err == nil {\n\t\t\t\t\tpowerMW = v\n\t\t\t\t\tgotPower = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn false\n\t}\n\tif !gotIdle && !gotPower {\n\t\treturn false\n\t}\n\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\tif _, ok := gm.GpuDataMap[appleGPUID]; !ok {\n\t\tgm.GpuDataMap[appleGPUID] = &system.GPUData{Name: \"Apple GPU\"}\n\t}\n\tgpu := gm.GpuDataMap[appleGPUID]\n\n\tif gotIdle {\n\t\t// Usage = 100 - idle residency (e.g. 100 - 99.03 = 0.97%)\n\t\tgpu.Usage += 100 - idleResidency\n\t}\n\tif gotPower {\n\t\t// mW -> W\n\t\tgpu.Power += powerMW / milliwattsInAWatt\n\t}\n\tgpu.Count++\n\treturn true\n}\n\n// startMacmonCollector runs `macmon pipe` in a loop and parses one JSON object per line.\n// This collector does not require sudo. A single logical GPU is reported as id \"0\".\nfunc (gm *GPUManager) startMacmonCollector() {\n\tif _, ok := gm.GpuDataMap[appleGPUID]; !ok {\n\t\tgm.GpuDataMap[appleGPUID] = &system.GPUData{Name: \"Apple GPU\"}\n\t}\n\n\tgo func() {\n\t\tfailures := 0\n\t\tfor {\n\t\t\tif err := gm.collectMacmonPipe(); err != nil {\n\t\t\t\tfailures++\n\t\t\t\tif failures > maxFailureRetries {\n\t\t\t\t\tslog.Warn(\"macmon GPU collector failed repeatedly, stopping\", \"err\", err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Error collecting macOS GPU data via macmon\", \"err\", err)\n\t\t\t\ttime.Sleep(retryWaitTime)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfailures = 0\n\t\t\t// `macmon pipe` is long-running; if it returns, wait a bit before restarting.\n\t\t\ttime.Sleep(retryWaitTime)\n\t\t}\n\t}()\n}\n\ntype macmonTemp struct {\n\tGPUTempAvg float64 `json:\"gpu_temp_avg\"`\n}\n\ntype macmonSample struct {\n\tGPUPower    float64    `json:\"gpu_power\"`     // watts (macmon reports fractional values)\n\tGPURAMPower float64    `json:\"gpu_ram_power\"` // watts\n\tGPUUsage    []float64  `json:\"gpu_usage\"`     // [freq_mhz, usage] where usage is typically 0..1\n\tTemp        macmonTemp `json:\"temp\"`\n}\n\nfunc (gm *GPUManager) collectMacmonPipe() (err error) {\n\tcmd := exec.Command(macmonCmd, \"pipe\", \"-i\", strconv.Itoa(macmonIntervalMs))\n\t// Avoid blocking if macmon writes to stderr.\n\tcmd.Stderr = io.Discard\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\t// Ensure we always reap the child to avoid zombies on any return path and\n\t// propagate a non-zero exit code if no other error was set.\n\tdefer func() {\n\t\t_ = stdout.Close()\n\t\tif cmd.ProcessState == nil || !cmd.ProcessState.Exited() {\n\t\t\t_ = cmd.Process.Kill()\n\t\t}\n\t\tif waitErr := cmd.Wait(); err == nil && waitErr != nil {\n\t\t\terr = waitErr\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(stdout)\n\tvar hadSample bool\n\tfor scanner.Scan() {\n\t\tline := bytes.TrimSpace(scanner.Bytes())\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif gm.parseMacmonLine(line) {\n\t\t\thadSample = true\n\t\t}\n\t}\n\tif scanErr := scanner.Err(); scanErr != nil {\n\t\treturn scanErr\n\t}\n\tif !hadSample {\n\t\treturn errNoValidData\n\t}\n\treturn nil\n}\n\n// parseMacmonLine parses a single macmon JSON line and updates Apple GPU metrics.\nfunc (gm *GPUManager) parseMacmonLine(line []byte) bool {\n\tvar sample macmonSample\n\tif err := json.Unmarshal(line, &sample); err != nil {\n\t\treturn false\n\t}\n\n\tusage := 0.0\n\tif len(sample.GPUUsage) >= 2 {\n\t\tusage = sample.GPUUsage[1]\n\t\t// Heuristic: macmon typically reports 0..1; convert to percentage.\n\t\tif usage <= 1.0 {\n\t\t\tusage *= 100\n\t\t}\n\t}\n\n\t// Consider the line valid if it contains at least one GPU metric.\n\tif usage == 0 && sample.GPUPower == 0 && sample.Temp.GPUTempAvg == 0 {\n\t\treturn false\n\t}\n\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\tgpu, ok := gm.GpuDataMap[appleGPUID]\n\tif !ok {\n\t\tgpu = &system.GPUData{Name: \"Apple GPU\"}\n\t\tgm.GpuDataMap[appleGPUID] = gpu\n\t}\n\tgpu.Temperature = sample.Temp.GPUTempAvg\n\tgpu.Usage += usage\n\t// macmon reports power in watts; include VRAM power if present.\n\tgpu.Power += sample.GPUPower + sample.GPURAMPower\n\tgpu.Count++\n\treturn true\n}\n"
  },
  {
    "path": "agent/gpu_darwin_test.go",
    "content": "//go:build darwin\n\npackage agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParsePowermetricsData(t *testing.T) {\n\tinput := `\nMachine model: Mac14,10\nOS version: 25D125\n\n*** Sampled system activity (Sat Feb 14 00:42:06 2026 -0500) (503.05ms elapsed) ***\n\n**** GPU usage ****\n\nGPU HW active frequency: 444 MHz\nGPU HW active residency:   0.97% (444 MHz: .97% 612 MHz:   0% 808 MHz:   0% 968 MHz:   0% 1110 MHz:   0% 1236 MHz:   0% 1338 MHz:   0% 1398 MHz:   0%)\nGPU SW requested state: (P1 : 100% P2 :   0% P3 :   0% P4 :   0% P5 :   0% P6 :   0% P7 :   0% P8 :   0%)\nGPU idle residency:  99.03%\nGPU Power: 4 mW\n`\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\tvalid := gm.parsePowermetricsData([]byte(input))\n\trequire.True(t, valid)\n\n\tg0, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"Apple GPU\", g0.Name)\n\t// Usage = 100 - 99.03 = 0.97\n\tassert.InDelta(t, 0.97, g0.Usage, 0.01)\n\t// 4 mW -> 0.004 W\n\tassert.InDelta(t, 0.004, g0.Power, 0.0001)\n\tassert.Equal(t, 1.0, g0.Count)\n}\n\nfunc TestParsePowermetricsDataPartial(t *testing.T) {\n\t// Only power line (e.g. older macOS or different sampler output)\n\tinput := `\n**** GPU usage ****\nGPU Power: 120 mW\n`\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\tvalid := gm.parsePowermetricsData([]byte(input))\n\trequire.True(t, valid)\n\n\tg0, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"Apple GPU\", g0.Name)\n\tassert.InDelta(t, 0.12, g0.Power, 0.001)\n\tassert.Equal(t, 1.0, g0.Count)\n}\n\nfunc TestParseMacmonLine(t *testing.T) {\n\tinput := `{\"all_power\":0.6468324661254883,\"ane_power\":0.0,\"cpu_power\":0.6359732151031494,\"ecpu_usage\":[2061,0.1726151406764984],\"gpu_power\":0.010859241709113121,\"gpu_ram_power\":0.000965250947047025,\"gpu_usage\":[503,0.013633215799927711],\"memory\":{\"ram_total\":17179869184,\"ram_usage\":12322914304,\"swap_total\":0,\"swap_usage\":0},\"pcpu_usage\":[1248,0.11792058497667313],\"ram_power\":0.14885640144348145,\"sys_power\":10.4955415725708,\"temp\":{\"cpu_temp_avg\":23.041261672973633,\"gpu_temp_avg\":29.44516944885254},\"timestamp\":\"2026-02-17T19:34:27.942556+00:00\"}`\n\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\tvalid := gm.parseMacmonLine([]byte(input))\n\trequire.True(t, valid)\n\n\tg0, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"Apple GPU\", g0.Name)\n\t// macmon reports usage fraction 0..1; expect percent conversion.\n\tassert.InDelta(t, 1.3633, g0.Usage, 0.05)\n\t// power includes gpu_power + gpu_ram_power\n\tassert.InDelta(t, 0.011824, g0.Power, 0.0005)\n\tassert.InDelta(t, 29.445, g0.Temperature, 0.01)\n\tassert.Equal(t, 1.0, g0.Count)\n}\n"
  },
  {
    "path": "agent/gpu_darwin_unsupported.go",
    "content": "//go:build !darwin\n\npackage agent\n\n// startPowermetricsCollector is a no-op on non-darwin platforms; the real implementation is in gpu_darwin.go.\nfunc (gm *GPUManager) startPowermetricsCollector() {}\n\n// startMacmonCollector is a no-op on non-darwin platforms; the real implementation is in gpu_darwin.go.\nfunc (gm *GPUManager) startMacmonCollector() {}\n"
  },
  {
    "path": "agent/gpu_intel.go",
    "content": "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\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nconst (\n\tintelGpuStatsCmd      string = \"intel_gpu_top\"\n\tintelGpuStatsInterval string = \"3300\" // in milliseconds\n)\n\ntype intelGpuStats struct {\n\tPowerGPU float64\n\tPowerPkg float64\n\tEngines  map[string]float64\n}\n\n// updateIntelFromStats updates aggregated GPU data from a single intelGpuStats sample\nfunc (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\t// only one gpu for now - cmd doesn't provide all by default\n\tid := \"i0\" // prefix with i to avoid conflicts with nvidia card ids\n\tgpuData, ok := gm.GpuDataMap[id]\n\tif !ok {\n\t\tgpuData = &system.GPUData{Name: \"GPU\", Engines: make(map[string]float64)}\n\t\tgm.GpuDataMap[id] = gpuData\n\t}\n\n\tgpuData.Power += sample.PowerGPU\n\tgpuData.PowerPkg += sample.PowerPkg\n\n\tif gpuData.Engines == nil {\n\t\tgpuData.Engines = make(map[string]float64, len(sample.Engines))\n\t}\n\tfor name, engine := range sample.Engines {\n\t\tgpuData.Engines[name] += engine\n\t}\n\n\tgpuData.Count++\n\treturn true\n}\n\n// collectIntelStats executes intel_gpu_top in text mode (-l) and parses the output\nfunc (gm *GPUManager) collectIntelStats() (err error) {\n\t// Build command arguments, optionally selecting a device via -d\n\targs := []string{\"-s\", intelGpuStatsInterval, \"-l\"}\n\tif dev, ok := utils.GetEnv(\"INTEL_GPU_DEVICE\"); ok && dev != \"\" {\n\t\targs = append(args, \"-d\", dev)\n\t}\n\tcmd := exec.Command(intelGpuStatsCmd, args...)\n\t// Avoid blocking if intel_gpu_top writes to stderr\n\tcmd.Stderr = io.Discard\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\t// Ensure we always reap the child to avoid zombies on any return path and\n\t// propagate a non-zero exit code if no other error was set.\n\tdefer func() {\n\t\t// Best-effort close of the pipe (unblock the child if it writes)\n\t\t_ = stdout.Close()\n\t\tif cmd.ProcessState == nil || !cmd.ProcessState.Exited() {\n\t\t\t_ = cmd.Process.Kill()\n\t\t}\n\t\tif waitErr := cmd.Wait(); err == nil && waitErr != nil {\n\t\t\terr = waitErr\n\t\t}\n\t}()\n\n\tscanner := bufio.NewScanner(stdout)\n\tvar header1 string\n\tvar engineNames []string\n\tvar friendlyNames []string\n\tvar preEngineCols int\n\tvar powerIndex int\n\tvar hadDataRow bool\n\t// skip first data row because it sometimes has erroneous data\n\tvar skippedFirstDataRow bool\n\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// first header line\n\t\tif strings.HasPrefix(line, \"Freq\") {\n\t\t\theader1 = line\n\t\t\tcontinue\n\t\t}\n\n\t\t// second header line\n\t\tif strings.HasPrefix(line, \"req\") {\n\t\t\tengineNames, friendlyNames, powerIndex, preEngineCols = gm.parseIntelHeaders(header1, line)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Data row\n\t\tif !skippedFirstDataRow {\n\t\t\tskippedFirstDataRow = true\n\t\t\tcontinue\n\t\t}\n\t\tsample, err := gm.parseIntelData(line, engineNames, friendlyNames, powerIndex, preEngineCols)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\thadDataRow = true\n\t\tgm.updateIntelFromStats(&sample)\n\t}\n\tif scanErr := scanner.Err(); scanErr != nil {\n\t\treturn scanErr\n\t}\n\tif !hadDataRow {\n\t\treturn errNoValidData\n\t}\n\treturn nil\n}\n\nfunc (gm *GPUManager) parseIntelHeaders(header1 string, header2 string) (engineNames []string, friendlyNames []string, powerIndex int, preEngineCols int) {\n\t// Build indexes\n\th1 := strings.Fields(header1)\n\th2 := strings.Fields(header2)\n\tpowerIndex = -1 // Initialize to -1, will be set to actual index if found\n\t// Collect engine names from header1\n\tfor _, col := range h1 {\n\t\tkey := strings.TrimRightFunc(col, func(r rune) bool {\n\t\t\treturn (r >= '0' && r <= '9') || r == '/'\n\t\t})\n\t\tvar friendly string\n\t\tswitch key {\n\t\tcase \"RCS\":\n\t\t\tfriendly = \"Render/3D\"\n\t\tcase \"BCS\":\n\t\t\tfriendly = \"Blitter\"\n\t\tcase \"VCS\":\n\t\t\tfriendly = \"Video\"\n\t\tcase \"VECS\":\n\t\t\tfriendly = \"VideoEnhance\"\n\t\tcase \"CCS\":\n\t\t\tfriendly = \"Compute\"\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tengineNames = append(engineNames, key)\n\t\tfriendlyNames = append(friendlyNames, friendly)\n\t}\n\t// find power gpu index among pre-engine columns\n\tif n := len(engineNames); n > 0 {\n\t\tpreEngineCols = max(len(h2)-3*n, 0)\n\t\tlimit := min(len(h2), preEngineCols)\n\t\tfor i := range limit {\n\t\t\tif strings.EqualFold(h2[i], \"gpu\") {\n\t\t\t\tpowerIndex = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn engineNames, friendlyNames, powerIndex, preEngineCols\n}\n\nfunc (gm *GPUManager) parseIntelData(line string, engineNames []string, friendlyNames []string, powerIndex int, preEngineCols int) (sample intelGpuStats, err error) {\n\tfields := strings.Fields(line)\n\tif len(fields) == 0 {\n\t\treturn sample, errNoValidData\n\t}\n\t// Make sure row has enough columns for engines\n\tif need := preEngineCols + 3*len(engineNames); len(fields) < need {\n\t\treturn sample, errNoValidData\n\t}\n\tif powerIndex >= 0 && powerIndex < len(fields) {\n\t\tif v, perr := strconv.ParseFloat(fields[powerIndex], 64); perr == nil {\n\t\t\tsample.PowerGPU = v\n\t\t}\n\t\tif v, perr := strconv.ParseFloat(fields[powerIndex+1], 64); perr == nil {\n\t\t\tsample.PowerPkg = v\n\t\t}\n\t}\n\tif len(engineNames) > 0 {\n\t\tsample.Engines = make(map[string]float64, len(engineNames))\n\t\tfor k := range engineNames {\n\t\t\tbase := preEngineCols + 3*k\n\t\t\tif base < len(fields) {\n\t\t\t\tbusy := 0.0\n\t\t\t\tif v, e := strconv.ParseFloat(fields[base], 64); e == nil {\n\t\t\t\t\tbusy = v\n\t\t\t\t}\n\t\t\t\tcur := sample.Engines[friendlyNames[k]]\n\t\t\t\tsample.Engines[friendlyNames[k]] = cur + busy\n\t\t\t} else {\n\t\t\t\tsample.Engines[friendlyNames[k]] = 0\n\t\t\t}\n\t\t}\n\t}\n\treturn sample, nil\n}\n"
  },
  {
    "path": "agent/gpu_nvml.go",
    "content": "//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\"unsafe\"\n\n\t\"github.com/ebitengine/purego\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\n// NVML constants and types\nconst (\n\tnvmlSuccess int = 0\n)\n\ntype nvmlDevice uintptr\n\ntype nvmlReturn int\n\ntype nvmlMemoryV1 struct {\n\tTotal uint64\n\tFree  uint64\n\tUsed  uint64\n}\n\ntype nvmlMemoryV2 struct {\n\tVersion  uint32\n\tTotal    uint64\n\tReserved uint64\n\tFree     uint64\n\tUsed     uint64\n}\n\ntype nvmlUtilization struct {\n\tGpu    uint32\n\tMemory uint32\n}\n\ntype nvmlPciInfo struct {\n\tBusId          [16]byte\n\tDomain         uint32\n\tBus            uint32\n\tDevice         uint32\n\tPciDeviceId    uint32\n\tPciSubSystemId uint32\n}\n\n// NVML function signatures\nvar (\n\tnvmlInit                      func() nvmlReturn\n\tnvmlShutdown                  func() nvmlReturn\n\tnvmlDeviceGetCount            func(count *uint32) nvmlReturn\n\tnvmlDeviceGetHandleByIndex    func(index uint32, device *nvmlDevice) nvmlReturn\n\tnvmlDeviceGetName             func(device nvmlDevice, name *byte, length uint32) nvmlReturn\n\tnvmlDeviceGetMemoryInfo       func(device nvmlDevice, memory uintptr) nvmlReturn\n\tnvmlDeviceGetUtilizationRates func(device nvmlDevice, utilization *nvmlUtilization) nvmlReturn\n\tnvmlDeviceGetTemperature      func(device nvmlDevice, sensorType int, temp *uint32) nvmlReturn\n\tnvmlDeviceGetPowerUsage       func(device nvmlDevice, power *uint32) nvmlReturn\n\tnvmlDeviceGetPciInfo          func(device nvmlDevice, pci *nvmlPciInfo) nvmlReturn\n\tnvmlErrorString               func(result nvmlReturn) string\n)\n\ntype nvmlCollector struct {\n\tgm      *GPUManager\n\tlib     uintptr\n\tdevices []nvmlDevice\n\tbdfs    []string\n\tisV2    bool\n}\n\nfunc (c *nvmlCollector) init() error {\n\tslog.Debug(\"NVML: Initializing\")\n\tlibPath := getNVMLPath()\n\n\tlib, err := openLibrary(libPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load %s: %w\", libPath, err)\n\t}\n\tc.lib = lib\n\n\tpurego.RegisterLibFunc(&nvmlInit, lib, \"nvmlInit\")\n\tpurego.RegisterLibFunc(&nvmlShutdown, lib, \"nvmlShutdown\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetCount, lib, \"nvmlDeviceGetCount\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetHandleByIndex, lib, \"nvmlDeviceGetHandleByIndex\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetName, lib, \"nvmlDeviceGetName\")\n\t// Try to get v2 memory info, fallback to v1 if not available\n\tif hasSymbol(lib, \"nvmlDeviceGetMemoryInfo_v2\") {\n\t\tc.isV2 = true\n\t\tpurego.RegisterLibFunc(&nvmlDeviceGetMemoryInfo, lib, \"nvmlDeviceGetMemoryInfo_v2\")\n\t} else {\n\t\tpurego.RegisterLibFunc(&nvmlDeviceGetMemoryInfo, lib, \"nvmlDeviceGetMemoryInfo\")\n\t}\n\tpurego.RegisterLibFunc(&nvmlDeviceGetUtilizationRates, lib, \"nvmlDeviceGetUtilizationRates\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetTemperature, lib, \"nvmlDeviceGetTemperature\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetPowerUsage, lib, \"nvmlDeviceGetPowerUsage\")\n\tpurego.RegisterLibFunc(&nvmlDeviceGetPciInfo, lib, \"nvmlDeviceGetPciInfo\")\n\tpurego.RegisterLibFunc(&nvmlErrorString, lib, \"nvmlErrorString\")\n\n\tif ret := nvmlInit(); ret != nvmlReturn(nvmlSuccess) {\n\t\treturn fmt.Errorf(\"nvmlInit failed: %v\", ret)\n\t}\n\n\tvar count uint32\n\tif ret := nvmlDeviceGetCount(&count); ret != nvmlReturn(nvmlSuccess) {\n\t\treturn fmt.Errorf(\"nvmlDeviceGetCount failed: %v\", ret)\n\t}\n\n\tfor i := uint32(0); i < count; i++ {\n\t\tvar device nvmlDevice\n\t\tif ret := nvmlDeviceGetHandleByIndex(i, &device); ret == nvmlReturn(nvmlSuccess) {\n\t\t\tc.devices = append(c.devices, device)\n\t\t\t// Get BDF for power state check\n\t\t\tvar pci nvmlPciInfo\n\t\t\tif ret := nvmlDeviceGetPciInfo(device, &pci); ret == nvmlReturn(nvmlSuccess) {\n\t\t\t\tbusID := string(pci.BusId[:])\n\t\t\t\tif idx := strings.Index(busID, \"\\x00\"); idx != -1 {\n\t\t\t\t\tbusID = busID[:idx]\n\t\t\t\t}\n\t\t\t\tc.bdfs = append(c.bdfs, strings.ToLower(busID))\n\t\t\t} else {\n\t\t\t\tc.bdfs = append(c.bdfs, \"\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *nvmlCollector) start() {\n\tdefer nvmlShutdown()\n\tticker := time.Tick(3 * time.Second)\n\n\tfor range ticker {\n\t\tc.collect()\n\t}\n}\n\nfunc (c *nvmlCollector) collect() {\n\tc.gm.Lock()\n\tdefer c.gm.Unlock()\n\n\tfor i, device := range c.devices {\n\t\tid := fmt.Sprintf(\"%d\", i)\n\t\tbdf := c.bdfs[i]\n\n\t\t// Update GPUDataMap\n\t\tif _, ok := c.gm.GpuDataMap[id]; !ok {\n\t\t\tvar nameBuf [64]byte\n\t\t\tif ret := nvmlDeviceGetName(device, &nameBuf[0], 64); ret != nvmlReturn(nvmlSuccess) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := string(nameBuf[:strings.Index(string(nameBuf[:]), \"\\x00\")])\n\t\t\tname = strings.TrimPrefix(name, \"NVIDIA \")\n\t\t\tc.gm.GpuDataMap[id] = &system.GPUData{Name: strings.TrimSuffix(name, \" Laptop GPU\")}\n\t\t}\n\t\tgpu := c.gm.GpuDataMap[id]\n\n\t\tif bdf != \"\" && !c.isGPUActive(bdf) {\n\t\t\tslog.Debug(\"NVML: GPU is suspended, skipping\", \"bdf\", bdf)\n\t\t\tgpu.Temperature = 0\n\t\t\tgpu.MemoryUsed = 0\n\t\t\tcontinue\n\t\t}\n\n\t\t// Utilization\n\t\tvar utilization nvmlUtilization\n\t\tif ret := nvmlDeviceGetUtilizationRates(device, &utilization); ret != nvmlReturn(nvmlSuccess) {\n\t\t\tslog.Debug(\"NVML: Utilization failed (GPU likely suspended)\", \"bdf\", bdf, \"ret\", ret)\n\t\t\tgpu.Temperature = 0\n\t\t\tgpu.MemoryUsed = 0\n\t\t\tcontinue\n\t\t}\n\n\t\tslog.Debug(\"NVML: Collecting data for GPU\", \"bdf\", bdf)\n\n\t\t// Temperature\n\t\tvar temp uint32\n\t\tnvmlDeviceGetTemperature(device, 0, &temp) // 0 is NVML_TEMPERATURE_GPU\n\n\t\t// Memory: only poll if GPU is active to avoid leaving D3cold state (#1522)\n\t\tif utilization.Gpu > 0 {\n\t\t\tvar usedMem, totalMem uint64\n\t\t\tif c.isV2 {\n\t\t\t\tvar memory nvmlMemoryV2\n\t\t\t\tmemory.Version = 0x02000028 // (2 << 24) | 40 bytes\n\t\t\t\tif ret := nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory))); ret != nvmlReturn(nvmlSuccess) {\n\t\t\t\t\tslog.Debug(\"NVML: MemoryInfo_v2 failed\", \"bdf\", bdf, \"ret\", ret)\n\t\t\t\t} else {\n\t\t\t\t\tusedMem = memory.Used\n\t\t\t\t\ttotalMem = memory.Total\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvar memory nvmlMemoryV1\n\t\t\t\tif ret := nvmlDeviceGetMemoryInfo(device, uintptr(unsafe.Pointer(&memory))); ret != nvmlReturn(nvmlSuccess) {\n\t\t\t\t\tslog.Debug(\"NVML: MemoryInfo failed\", \"bdf\", bdf, \"ret\", ret)\n\t\t\t\t} else {\n\t\t\t\t\tusedMem = memory.Used\n\t\t\t\t\ttotalMem = memory.Total\n\t\t\t\t}\n\t\t\t}\n\t\t\tif totalMem > 0 {\n\t\t\t\tgpu.MemoryUsed = float64(usedMem) / 1024 / 1024 / mebibytesInAMegabyte\n\t\t\t\tgpu.MemoryTotal = float64(totalMem) / 1024 / 1024 / mebibytesInAMegabyte\n\t\t\t}\n\t\t} else {\n\t\t\tslog.Debug(\"NVML: Skipping memory info (utilization=0)\", \"bdf\", bdf)\n\t\t}\n\n\t\t// Power\n\t\tvar power uint32\n\t\tnvmlDeviceGetPowerUsage(device, &power)\n\n\t\tgpu.Temperature = float64(temp)\n\t\tgpu.Usage += float64(utilization.Gpu)\n\t\tgpu.Power += float64(power) / 1000.0\n\t\tgpu.Count++\n\t\tslog.Debug(\"NVML: Collected data\", \"gpu\", gpu)\n\t}\n}\n"
  },
  {
    "path": "agent/gpu_nvml_linux.go",
    "content": "//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/ebitengine/purego\"\n)\n\nfunc openLibrary(name string) (uintptr, error) {\n\treturn purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL)\n}\n\nfunc getNVMLPath() string {\n\treturn \"libnvidia-ml.so.1\"\n}\n\nfunc hasSymbol(lib uintptr, symbol string) bool {\n\t_, err := purego.Dlsym(lib, symbol)\n\treturn err == nil\n}\n\nfunc (c *nvmlCollector) isGPUActive(bdf string) bool {\n\t// runtime_status\n\tstatusPath := filepath.Join(\"/sys/bus/pci/devices\", bdf, \"power/runtime_status\")\n\tstatus, err := os.ReadFile(statusPath)\n\tif err != nil {\n\t\tslog.Debug(\"NVML: Can't read runtime_status\", \"bdf\", bdf, \"err\", err)\n\t\treturn true // Assume active if we can't read status\n\t}\n\tstatusStr := strings.TrimSpace(string(status))\n\tif statusStr != \"active\" && statusStr != \"resuming\" {\n\t\tslog.Debug(\"NVML: GPU not active\", \"bdf\", bdf, \"status\", statusStr)\n\t\treturn false\n\t}\n\n\t// power_state (D0 check)\n\t// Find any drm card device power_state\n\tpstatePathPattern := filepath.Join(\"/sys/bus/pci/devices\", bdf, \"drm/card*/device/power_state\")\n\tmatches, _ := filepath.Glob(pstatePathPattern)\n\tif len(matches) > 0 {\n\t\tpstate, err := os.ReadFile(matches[0])\n\t\tif err == nil {\n\t\t\tpstateStr := strings.TrimSpace(string(pstate))\n\t\t\tif pstateStr != \"D0\" {\n\t\t\t\tslog.Debug(\"NVML: GPU not in D0 state\", \"bdf\", bdf, \"pstate\", pstateStr)\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "agent/gpu_nvml_unsupported.go",
    "content": "//go:build (!linux && !windows) || !amd64 || (linux && !glibc)\n\npackage agent\n\nimport \"fmt\"\n\ntype nvmlCollector struct {\n\tgm *GPUManager\n}\n\nfunc (c *nvmlCollector) init() error {\n\treturn fmt.Errorf(\"nvml not supported on this platform\")\n}\n\nfunc (c *nvmlCollector) start() {}\n"
  },
  {
    "path": "agent/gpu_nvml_windows.go",
    "content": "//go:build windows && amd64\n\npackage agent\n\nimport (\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc openLibrary(name string) (uintptr, error) {\n\thandle, err := windows.LoadLibrary(name)\n\treturn uintptr(handle), err\n}\n\nfunc getNVMLPath() string {\n\treturn \"nvml.dll\"\n}\n\nfunc hasSymbol(lib uintptr, symbol string) bool {\n\t_, err := windows.GetProcAddress(windows.Handle(lib), symbol)\n\treturn err == nil\n}\n\nfunc (c *nvmlCollector) isGPUActive(bdf string) bool {\n\treturn true\n}\n"
  },
  {
    "path": "agent/gpu_nvtop.go",
    "content": "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/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\ntype nvtopSnapshot struct {\n\tDeviceName string  `json:\"device_name\"`\n\tTemp       *string `json:\"temp\"`\n\tPowerDraw  *string `json:\"power_draw\"`\n\tGpuUtil    *string `json:\"gpu_util\"`\n\tMemTotal   *string `json:\"mem_total\"`\n\tMemUsed    *string `json:\"mem_used\"`\n}\n\n// parseNvtopNumber parses nvtop numeric strings with units (C/W/%).\nfunc parseNvtopNumber(raw string) float64 {\n\tcleaned := strings.TrimSpace(raw)\n\tcleaned = strings.TrimSuffix(cleaned, \"C\")\n\tcleaned = strings.TrimSuffix(cleaned, \"W\")\n\tcleaned = strings.TrimSuffix(cleaned, \"%\")\n\tval, _ := strconv.ParseFloat(cleaned, 64)\n\treturn val\n}\n\n// parseNvtopData parses a single nvtop JSON snapshot payload.\nfunc (gm *GPUManager) parseNvtopData(output []byte) bool {\n\tvar snapshots []nvtopSnapshot\n\tif err := json.Unmarshal(output, &snapshots); err != nil || len(snapshots) == 0 {\n\t\treturn false\n\t}\n\treturn gm.updateNvtopSnapshots(snapshots)\n}\n\n// updateNvtopSnapshots applies one decoded nvtop snapshot batch to GPU accumulators.\nfunc (gm *GPUManager) updateNvtopSnapshots(snapshots []nvtopSnapshot) bool {\n\tgm.Lock()\n\tdefer gm.Unlock()\n\n\tvalid := false\n\tusedIDs := make(map[string]struct{}, len(snapshots))\n\tfor i, sample := range snapshots {\n\t\tif sample.DeviceName == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tindexID := \"n\" + strconv.Itoa(i)\n\t\tid := indexID\n\n\t\t// nvtop ordering can change, so prefer reusing an existing slot with matching device name.\n\t\tif existingByIndex, ok := gm.GpuDataMap[indexID]; ok && existingByIndex.Name != \"\" && existingByIndex.Name != sample.DeviceName {\n\t\t\tfor existingID, gpu := range gm.GpuDataMap {\n\t\t\t\tif !strings.HasPrefix(existingID, \"n\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif _, taken := usedIDs[existingID]; taken {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif gpu.Name == sample.DeviceName {\n\t\t\t\t\tid = existingID\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif _, ok := gm.GpuDataMap[id]; !ok {\n\t\t\tgm.GpuDataMap[id] = &system.GPUData{Name: sample.DeviceName}\n\t\t}\n\t\tgpu := gm.GpuDataMap[id]\n\t\tgpu.Name = sample.DeviceName\n\n\t\tif sample.Temp != nil {\n\t\t\tgpu.Temperature = parseNvtopNumber(*sample.Temp)\n\t\t}\n\t\tif sample.MemUsed != nil {\n\t\t\tgpu.MemoryUsed = utils.BytesToMegabytes(parseNvtopNumber(*sample.MemUsed))\n\t\t}\n\t\tif sample.MemTotal != nil {\n\t\t\tgpu.MemoryTotal = utils.BytesToMegabytes(parseNvtopNumber(*sample.MemTotal))\n\t\t}\n\t\tif sample.GpuUtil != nil {\n\t\t\tgpu.Usage += parseNvtopNumber(*sample.GpuUtil)\n\t\t}\n\t\tif sample.PowerDraw != nil {\n\t\t\tgpu.Power += parseNvtopNumber(*sample.PowerDraw)\n\t\t}\n\t\tgpu.Count++\n\t\tusedIDs[id] = struct{}{}\n\t\tvalid = true\n\t}\n\treturn valid\n}\n\n// collectNvtopStats runs nvtop loop mode and continuously decodes JSON snapshots.\nfunc (gm *GPUManager) collectNvtopStats(interval string) error {\n\tcmd := exec.Command(nvtopCmd, \"-lP\", \"-d\", interval)\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = stdout.Close()\n\t\tif cmd.ProcessState == nil || !cmd.ProcessState.Exited() {\n\t\t\t_ = cmd.Process.Kill()\n\t\t}\n\t\t_ = cmd.Wait()\n\t}()\n\n\tdecoder := json.NewDecoder(stdout)\n\tfoundValid := false\n\tfor {\n\t\tvar snapshots []nvtopSnapshot\n\t\tif err := decoder.Decode(&snapshots); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tif foundValid {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn errNoValidData\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif gm.updateNvtopSnapshots(snapshots) {\n\t\t\tfoundValid = true\n\t\t}\n\t}\n}\n\n// startNvtopCollector starts nvtop collection with retry or fallback callback handling.\nfunc (gm *GPUManager) startNvtopCollector(interval string, onFailure func()) {\n\tgo func() {\n\t\tfailures := 0\n\t\tfor {\n\t\t\tif err := gm.collectNvtopStats(interval); err != nil {\n\t\t\t\tif onFailure != nil {\n\t\t\t\t\tslog.Warn(\"Error collecting GPU data via nvtop\", \"err\", err)\n\t\t\t\t\tonFailure()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfailures++\n\t\t\t\tif failures > maxFailureRetries {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Error collecting GPU data via nvtop\", \"err\", err)\n\t\t\t\ttime.Sleep(retryWaitTime)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "agent/gpu_test.go",
    "content": "//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/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseNvidiaData(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\twantData  map[string]system.GPUData\n\t\twantValid bool\n\t}{\n\t\t{\n\t\t\tname:  \"valid multi-gpu data\",\n\t\t\tinput: \"0, NVIDIA GeForce RTX 3050 Ti Laptop GPU, 48, 12, 4096, 26.3, 12.73\\n1, NVIDIA A100-PCIE-40GB, 38, 74, 40960, [N/A], 36.79\",\n\t\t\twantData: map[string]system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"GeForce RTX 3050 Ti\",\n\t\t\t\t\tTemperature: 48.0,\n\t\t\t\t\tMemoryUsed:  12.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 4096.0 / 1.024,\n\t\t\t\t\tUsage:       26.3,\n\t\t\t\t\tPower:       12.73,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"1\": {\n\t\t\t\t\tName:        \"A100-PCIE-40GB\",\n\t\t\t\t\tTemperature: 38.0,\n\t\t\t\t\tMemoryUsed:  74.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 40960.0 / 1.024,\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       36.79,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"more valid multi-gpu data\",\n\t\t\tinput: `0, NVIDIA A10, 45, 19676, 23028, 0, 58.98\n1, NVIDIA A10, 45, 19638, 23028, 0, 62.35\n2, NVIDIA A10, 44, 21700, 23028, 0, 59.57\n3, NVIDIA A10, 45, 18222, 23028, 0, 61.76`,\n\t\t\twantData: map[string]system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"A10\",\n\t\t\t\t\tTemperature: 45.0,\n\t\t\t\t\tMemoryUsed:  19676.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 23028.0 / 1.024,\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       58.98,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"1\": {\n\t\t\t\t\tName:        \"A10\",\n\t\t\t\t\tTemperature: 45.0,\n\t\t\t\t\tMemoryUsed:  19638.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 23028.0 / 1.024,\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       62.35,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"2\": {\n\t\t\t\t\tName:        \"A10\",\n\t\t\t\t\tTemperature: 44.0,\n\t\t\t\t\tMemoryUsed:  21700.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 23028.0 / 1.024,\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       59.57,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"3\": {\n\t\t\t\t\tName:        \"A10\",\n\t\t\t\t\tTemperature: 45.0,\n\t\t\t\t\tMemoryUsed:  18222.0 / 1.024,\n\t\t\t\t\tMemoryTotal: 23028.0 / 1.024,\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       61.76,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantValid: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty input\",\n\t\t\tinput:     \"\",\n\t\t\twantData:  map[string]system.GPUData{},\n\t\t\twantValid: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"malformed data\",\n\t\t\tinput:     \"bad, data, here\",\n\t\t\twantData:  map[string]system.GPUData{},\n\t\t\twantValid: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgm := &GPUManager{\n\t\t\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t\t\t}\n\t\t\tvalid := gm.parseNvidiaData([]byte(tt.input))\n\t\t\tassert.Equal(t, tt.wantValid, valid)\n\n\t\t\tif tt.wantValid {\n\t\t\t\tfor id, want := range tt.wantData {\n\t\t\t\t\tgot := gm.GpuDataMap[id]\n\t\t\t\t\trequire.NotNil(t, got)\n\t\t\t\t\tassert.Equal(t, want.Name, got.Name)\n\t\t\t\t\tassert.InDelta(t, want.Temperature, got.Temperature, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.MemoryUsed, got.MemoryUsed, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.MemoryTotal, got.MemoryTotal, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.Usage, got.Usage, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.Power, got.Power, 0.01)\n\t\t\t\t\tassert.Equal(t, want.Count, got.Count)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseAmdData(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\twantData  map[string]system.GPUData\n\t\twantValid bool\n\t}{\n\t\t{\n\t\t\tname: \"valid single gpu data\",\n\t\t\tinput: `{\n\t\t\t\t\"card0\": {\n\t\t\t\t\t\"GUID\": \"34756\",\n\t\t\t\t\t\"Temperature (Sensor edge) (C)\": \"47.0\",\n\t\t\t\t\t\"Current Socket Graphics Package Power (W)\": \"9.215\",\n\t\t\t\t\t\"GPU use (%)\": \"0\",\n\t\t\t\t\t\"VRAM Total Memory (B)\": \"536870912\",\n\t\t\t\t\t\"VRAM Total Used Memory (B)\": \"482263040\",\n\t\t\t\t\t\"Card Series\": \"Rembrandt [Radeon 680M]\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\twantData: map[string]system.GPUData{\n\t\t\t\t\"34756\": {\n\t\t\t\t\tName:        \"Rembrandt [Radeon 680M]\",\n\t\t\t\t\tTemperature: 47.0,\n\t\t\t\t\tMemoryUsed:  482263040.0 / (1024 * 1024),\n\t\t\t\t\tMemoryTotal: 536870912.0 / (1024 * 1024),\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       9.215,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantValid: true,\n\t\t},\n\t\t{\n\t\t\tname: \"valid multi gpu data\",\n\t\t\tinput: `{\n\t\t\t\t\"card0\": {\n\t\t\t\t\t\"GUID\": \"34756\",\n\t\t\t\t\t\"Temperature (Sensor edge) (C)\": \"47.0\",\n\t\t\t\t\t\"Current Socket Graphics Package Power (W)\": \"9.215\",\n\t\t\t\t\t\"GPU use (%)\": \"0\",\n\t\t\t\t\t\"VRAM Total Memory (B)\": \"536870912\",\n\t\t\t\t\t\"VRAM Total Used Memory (B)\": \"482263040\",\n\t\t\t\t\t\"Card Series\": \"Rembrandt [Radeon 680M]\"\n\t\t\t\t},\n\t\t\t\t\"card1\": {\n\t\t\t\t\t\"GUID\": \"38294\",\n\t\t\t\t\t\"Temperature (Sensor edge) (C)\": \"49.0\",\n\t\t\t\t\t\"Temperature (Sensor junction) (C)\": \"49.0\",\n\t\t\t\t\t\"Temperature (Sensor memory) (C)\": \"62.0\",\n\t\t\t\t\t\"Average Graphics Package Power (W)\": \"19.0\",\n\t\t\t\t\t\"GPU use (%)\": \"20.3\",\n\t\t\t\t\t\"VRAM Total Memory (B)\": \"25753026560\",\n\t\t\t\t\t\"VRAM Total Used Memory (B)\": \"794341376\",\n\t\t\t\t\t\"Card Series\": \"Navi 31 [Radeon RX 7900 XT]\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\twantData: map[string]system.GPUData{\n\t\t\t\t\"34756\": {\n\t\t\t\t\tName:        \"Rembrandt [Radeon 680M]\",\n\t\t\t\t\tTemperature: 47.0,\n\t\t\t\t\tMemoryUsed:  482263040.0 / (1024 * 1024),\n\t\t\t\t\tMemoryTotal: 536870912.0 / (1024 * 1024),\n\t\t\t\t\tUsage:       0.0,\n\t\t\t\t\tPower:       9.215,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"38294\": {\n\t\t\t\t\tName:        \"Navi 31 [Radeon RX 7900 XT]\",\n\t\t\t\t\tTemperature: 49.0,\n\t\t\t\t\tMemoryUsed:  794341376.0 / (1024 * 1024),\n\t\t\t\t\tMemoryTotal: 25753026560.0 / (1024 * 1024),\n\t\t\t\t\tUsage:       20.3,\n\t\t\t\t\tPower:       19.0,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantValid: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid json\",\n\t\t\tinput: \"{bad json\",\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid json\",\n\t\t\tinput:     \"{bad json\",\n\t\t\twantData:  map[string]system.GPUData{},\n\t\t\twantValid: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgm := &GPUManager{\n\t\t\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t\t\t}\n\t\t\tvalid := gm.parseAmdData([]byte(tt.input))\n\t\t\tassert.Equal(t, tt.wantValid, valid)\n\n\t\t\tif tt.wantValid {\n\t\t\t\tfor id, want := range tt.wantData {\n\t\t\t\t\tgot := gm.GpuDataMap[id]\n\t\t\t\t\trequire.NotNil(t, got)\n\t\t\t\t\tassert.Equal(t, want.Name, got.Name)\n\t\t\t\t\tassert.InDelta(t, want.Temperature, got.Temperature, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.MemoryUsed, got.MemoryUsed, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.MemoryTotal, got.MemoryTotal, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.Usage, got.Usage, 0.01)\n\t\t\t\t\tassert.InDelta(t, want.Power, got.Power, 0.01)\n\t\t\t\t\tassert.Equal(t, want.Count, got.Count)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseNvtopData(t *testing.T) {\n\tinput, err := os.ReadFile(\"test-data/nvtop.json\")\n\trequire.NoError(t, err)\n\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\tvalid := gm.parseNvtopData(input)\n\trequire.True(t, valid)\n\n\tg0, ok := gm.GpuDataMap[\"n0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\", g0.Name)\n\tassert.Equal(t, 48.0, g0.Temperature)\n\tassert.Equal(t, 5.0, g0.Usage)\n\tassert.Equal(t, 13.0, g0.Power)\n\tassert.Equal(t, utils.BytesToMegabytes(349372416), g0.MemoryUsed)\n\tassert.Equal(t, utils.BytesToMegabytes(4294967296), g0.MemoryTotal)\n\tassert.Equal(t, 1.0, g0.Count)\n\n\tg1, ok := gm.GpuDataMap[\"n1\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"AMD Radeon 680M\", g1.Name)\n\tassert.Equal(t, 48.0, g1.Temperature)\n\tassert.Equal(t, 12.0, g1.Usage)\n\tassert.Equal(t, 9.0, g1.Power)\n\tassert.Equal(t, utils.BytesToMegabytes(1213784064), g1.MemoryUsed)\n\tassert.Equal(t, utils.BytesToMegabytes(16929173504), g1.MemoryTotal)\n\tassert.Equal(t, 1.0, g1.Count)\n}\n\nfunc TestUpdateNvtopSnapshotsKeepsDeviceAssociationWhenOrderChanges(t *testing.T) {\n\tstrPtr := func(s string) *string { return &s }\n\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\n\tfirstBatch := []nvtopSnapshot{\n\t\t{\n\t\t\tDeviceName: \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\",\n\t\t\tGpuUtil:    strPtr(\"20%\"),\n\t\t\tPowerDraw:  strPtr(\"10W\"),\n\t\t},\n\t\t{\n\t\t\tDeviceName: \"AMD Radeon 680M\",\n\t\t\tGpuUtil:    strPtr(\"30%\"),\n\t\t\tPowerDraw:  strPtr(\"20W\"),\n\t\t},\n\t}\n\tsecondBatchSwapped := []nvtopSnapshot{\n\t\t{\n\t\t\tDeviceName: \"AMD Radeon 680M\",\n\t\t\tGpuUtil:    strPtr(\"40%\"),\n\t\t\tPowerDraw:  strPtr(\"25W\"),\n\t\t},\n\t\t{\n\t\t\tDeviceName: \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\",\n\t\t\tGpuUtil:    strPtr(\"50%\"),\n\t\t\tPowerDraw:  strPtr(\"15W\"),\n\t\t},\n\t}\n\n\trequire.True(t, gm.updateNvtopSnapshots(firstBatch))\n\trequire.True(t, gm.updateNvtopSnapshots(secondBatchSwapped))\n\n\tnvidia := gm.GpuDataMap[\"n0\"]\n\trequire.NotNil(t, nvidia)\n\tassert.Equal(t, \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\", nvidia.Name)\n\tassert.Equal(t, 70.0, nvidia.Usage)\n\tassert.Equal(t, 25.0, nvidia.Power)\n\tassert.Equal(t, 2.0, nvidia.Count)\n\n\tamd := gm.GpuDataMap[\"n1\"]\n\trequire.NotNil(t, amd)\n\tassert.Equal(t, \"AMD Radeon 680M\", amd.Name)\n\tassert.Equal(t, 70.0, amd.Usage)\n\tassert.Equal(t, 45.0, amd.Power)\n\tassert.Equal(t, 2.0, amd.Count)\n}\n\nfunc TestParseCollectorPriority(t *testing.T) {\n\tgot := parseCollectorPriority(\" nvml, nvidia-smi, intel_gpu_top, amd_sysfs, nvtop, rocm-smi, bad \")\n\twant := []collectorSource{\n\t\tcollectorSourceNVML,\n\t\tcollectorSourceNvidiaSMI,\n\t\tcollectorSourceIntelGpuTop,\n\t\tcollectorSourceAmdSysfs,\n\t\tcollectorSourceNVTop,\n\t\tcollectorSourceRocmSMI,\n\t}\n\tassert.Equal(t, want, got)\n}\n\nfunc TestParseJetsonData(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\twantMetrics *system.GPUData\n\t}{\n\t\t{\n\t\t\tname:  \"valid data\",\n\t\t\tinput: \"11-14-2024 22:54:33 RAM 4300/30698MB GR3D_FREQ 45% tj@52.468C VDD_GPU_SOC 2171mW\",\n\t\t\twantMetrics: &system.GPUData{\n\t\t\t\tName:        \"GPU\",\n\t\t\t\tMemoryUsed:  4300.0,\n\t\t\t\tMemoryTotal: 30698.0,\n\t\t\t\tUsage:       45.0,\n\t\t\t\tTemperature: 52.468,\n\t\t\t\tPower:       2.171,\n\t\t\t\tCount:       1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"more valid data\",\n\t\t\tinput: \"11-15-2024 08:38:09 RAM 6185/7620MB (lfb 8x2MB) SWAP 851/3810MB (cached 1MB) CPU [15%@729,11%@729,14%@729,13%@729,11%@729,8%@729] EMC_FREQ 43%@2133 GR3D_FREQ 63%@[621] NVDEC off NVJPG off NVJPG1 off VIC off OFA off APE 200 cpu@53.968C soc2@52.437C soc0@50.75C gpu@53.343C tj@53.968C soc1@51.656C VDD_IN 12479mW/12479mW VDD_CPU_GPU_CV 4667mW/4667mW VDD_SOC 2817mW/2817mW\",\n\t\t\twantMetrics: &system.GPUData{\n\t\t\t\tName:        \"GPU\",\n\t\t\t\tMemoryUsed:  6185.0,\n\t\t\t\tMemoryTotal: 7620.0,\n\t\t\t\tUsage:       63.0,\n\t\t\t\tTemperature: 53.968,\n\t\t\t\tPower:       4.667,\n\t\t\t\tCount:       1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"orin nano\",\n\t\t\tinput: \"06-18-2025 11:25:24 RAM 3452/7620MB (lfb 25x4MB) SWAP 1518/16384MB (cached 174MB) CPU [1%@1420,2%@1420,0%@1420,2%@1420,2%@729,1%@729] GR3D_FREQ 0% cpu@50.031C soc2@49.031C soc0@50C gpu@49.031C tj@50.25C soc1@50.25C VDD_IN 4824mW/4824mW VDD_CPU_GPU_CV 518mW/518mW VDD_SOC 1475mW/1475mW\",\n\t\t\twantMetrics: &system.GPUData{\n\t\t\t\tName:        \"GPU\",\n\t\t\t\tMemoryUsed:  3452.0,\n\t\t\t\tMemoryTotal: 7620.0,\n\t\t\t\tUsage:       0.0,\n\t\t\t\tTemperature: 50.25,\n\t\t\t\tPower:       0.518,\n\t\t\t\tCount:       1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"missing temperature\",\n\t\t\tinput: \"11-14-2024 22:54:33 RAM 4300/30698MB GR3D_FREQ 45% VDD_GPU_SOC 2171mW\",\n\t\t\twantMetrics: &system.GPUData{\n\t\t\t\tName:        \"GPU\",\n\t\t\t\tMemoryUsed:  4300.0,\n\t\t\t\tMemoryTotal: 30698.0,\n\t\t\t\tUsage:       45.0,\n\t\t\t\tPower:       2.171,\n\t\t\t\tCount:       1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"orin-style output with GPU@ temp and VDD_SYS_GPU power\",\n\t\t\tinput: \"RAM 3276/7859MB (lfb 5x4MB) SWAP 1626/12122MB (cached 181MB) CPU [44%@1421,49%@2031,67%@2034,17%@1420,25%@1419,8%@1420] EMC_FREQ 1%@1866 GR3D_FREQ 0%@114 APE 150 MTS fg 1% bg 1% PLL@42.5C MCPU@42.5C PMIC@50C Tboard@38C GPU@39.5C BCPU@42.5C thermal@41.3C Tdiode@39.25C VDD_SYS_GPU 182/182 VDD_SYS_SOC 730/730 VDD_4V0_WIFI 0/0 VDD_IN 5297/5297 VDD_SYS_CPU 1917/1917 VDD_SYS_DDR 1241/1241\",\n\t\t\twantMetrics: &system.GPUData{\n\t\t\t\tName:        \"GPU\",\n\t\t\t\tMemoryUsed:  3276.0,\n\t\t\t\tMemoryTotal: 7859.0,\n\t\t\t\tUsage:       0.0,\n\t\t\t\tPower:       0.182, // 182mW -> 0.182W\n\t\t\t\tTemperature: 39.5,\n\t\t\t\tCount:       1,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgm := &GPUManager{\n\t\t\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t\t\t}\n\t\t\tparser := gm.getJetsonParser()\n\t\t\tvalid := parser([]byte(tt.input))\n\t\t\tassert.Equal(t, true, valid)\n\n\t\t\tgot := gm.GpuDataMap[\"0\"]\n\t\t\trequire.NotNil(t, got)\n\t\t\tassert.Equal(t, tt.wantMetrics.Name, got.Name)\n\t\t\tassert.InDelta(t, tt.wantMetrics.MemoryUsed, got.MemoryUsed, 0.01)\n\t\t\tassert.InDelta(t, tt.wantMetrics.MemoryTotal, got.MemoryTotal, 0.01)\n\t\t\tassert.InDelta(t, tt.wantMetrics.Usage, got.Usage, 0.01)\n\t\t\tif tt.wantMetrics.Temperature > 0 {\n\t\t\t\tassert.InDelta(t, tt.wantMetrics.Temperature, got.Temperature, 0.01)\n\t\t\t}\n\t\t\tassert.InDelta(t, tt.wantMetrics.Power, got.Power, 0.01)\n\t\t\tassert.Equal(t, tt.wantMetrics.Count, got.Count)\n\t\t})\n\t}\n}\n\nfunc TestGetCurrentData(t *testing.T) {\n\tt.Run(\"calculates averages with per-cache-key delta tracking\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"GPU1\",\n\t\t\t\t\tTemperature: 50,\n\t\t\t\t\tMemoryUsed:  2048,\n\t\t\t\t\tMemoryTotal: 4096,\n\t\t\t\t\tUsage:       100, // 100 over 2 counts = 50 avg\n\t\t\t\t\tPower:       200, // 200 over 2 counts = 100 avg\n\t\t\t\t\tCount:       2,\n\t\t\t\t},\n\t\t\t\t\"1\": {\n\t\t\t\t\tName:        \"GPU1\",\n\t\t\t\t\tTemperature: 60,\n\t\t\t\t\tMemoryUsed:  3072,\n\t\t\t\t\tMemoryTotal: 8192,\n\t\t\t\t\tUsage:       30,\n\t\t\t\t\tPower:       60,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t\t\"2\": {\n\t\t\t\t\tName:        \"GPU 2\",\n\t\t\t\t\tTemperature: 70,\n\t\t\t\t\tMemoryUsed:  4096,\n\t\t\t\t\tMemoryTotal: 8192,\n\t\t\t\t\tUsage:       200,\n\t\t\t\t\tPower:       400,\n\t\t\t\t\tCount:       1,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcacheKey := uint16(5000)\n\t\tresult := gm.GetCurrentData(cacheKey)\n\n\t\t// Verify name disambiguation\n\t\tassert.Equal(t, \"GPU1 0\", result[\"0\"].Name)\n\t\tassert.Equal(t, \"GPU1 1\", result[\"1\"].Name)\n\t\tassert.Equal(t, \"GPU 2\", result[\"2\"].Name)\n\n\t\t// Check averaged values in the result\n\t\tassert.InDelta(t, 50.0, result[\"0\"].Usage, 0.01)\n\t\tassert.InDelta(t, 100.0, result[\"0\"].Power, 0.01)\n\t\tassert.InDelta(t, 30.0, result[\"1\"].Usage, 0.01)\n\t\tassert.InDelta(t, 60.0, result[\"1\"].Power, 0.01)\n\n\t\t// Verify that accumulators in the original map are NOT reset (they keep growing)\n\t\tassert.EqualValues(t, 2, gm.GpuDataMap[\"0\"].Count, \"GPU 0 Count should remain at 2\")\n\t\tassert.EqualValues(t, 100, gm.GpuDataMap[\"0\"].Usage, \"GPU 0 Usage should remain at 100\")\n\t\tassert.Equal(t, 200.0, gm.GpuDataMap[\"0\"].Power, \"GPU 0 Power should remain at 200\")\n\t\tassert.Equal(t, 1.0, gm.GpuDataMap[\"1\"].Count, \"GPU 1 Count should remain at 1\")\n\t\tassert.Equal(t, 30.0, gm.GpuDataMap[\"1\"].Usage, \"GPU 1 Usage should remain at 30\")\n\t\tassert.Equal(t, 60.0, gm.GpuDataMap[\"1\"].Power, \"GPU 1 Power should remain at 60\")\n\n\t\t// Verify snapshots were stored for this cache key\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey][\"0\"])\n\t\tassert.Equal(t, uint32(2), gm.lastSnapshots[cacheKey][\"0\"].count)\n\t\tassert.Equal(t, 100.0, gm.lastSnapshots[cacheKey][\"0\"].usage)\n\t\tassert.Equal(t, 200.0, gm.lastSnapshots[cacheKey][\"0\"].power)\n\t})\n\n\tt.Run(\"handles zero count without panicking\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:  \"TestGPU\",\n\t\t\t\t\tCount: 0,\n\t\t\t\t\tUsage: 0,\n\t\t\t\t\tPower: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcacheKey := uint16(5000)\n\t\tvar result map[string]system.GPUData\n\t\tassert.NotPanics(t, func() {\n\t\t\tresult = gm.GetCurrentData(cacheKey)\n\t\t})\n\n\t\t// Check that usage and power are 0\n\t\tassert.Equal(t, 0.0, result[\"0\"].Usage)\n\t\tassert.Equal(t, 0.0, result[\"0\"].Power)\n\n\t\t// Verify count remains 0\n\t\tassert.EqualValues(t, 0, gm.GpuDataMap[\"0\"].Count)\n\t})\n\n\tt.Run(\"uses last average when no new data arrives\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"TestGPU\",\n\t\t\t\t\tTemperature: 55.0,\n\t\t\t\t\tMemoryUsed:  1500,\n\t\t\t\t\tMemoryTotal: 8000,\n\t\t\t\t\tUsage:       100, // Will average to 50\n\t\t\t\t\tPower:       200, // Will average to 100\n\t\t\t\t\tCount:       2,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcacheKey := uint16(5000)\n\n\t\t// First collection - should calculate averages and store them\n\t\tresult1 := gm.GetCurrentData(cacheKey)\n\t\tassert.InDelta(t, 50.0, result1[\"0\"].Usage, 0.01)\n\t\tassert.InDelta(t, 100.0, result1[\"0\"].Power, 0.01)\n\t\tassert.EqualValues(t, 2, gm.GpuDataMap[\"0\"].Count, \"Count should remain at 2\")\n\n\t\t// Update temperature but no new usage/power data (count stays same)\n\t\tgm.GpuDataMap[\"0\"].Temperature = 60.0\n\t\tgm.GpuDataMap[\"0\"].MemoryUsed = 1600\n\n\t\t// Second collection - should use last averages since count hasn't changed (delta = 0)\n\t\tresult2 := gm.GetCurrentData(cacheKey)\n\t\tassert.InDelta(t, 50.0, result2[\"0\"].Usage, 0.01, \"Should use last average\")\n\t\tassert.InDelta(t, 100.0, result2[\"0\"].Power, 0.01, \"Should use last average\")\n\t\tassert.InDelta(t, 60.0, result2[\"0\"].Temperature, 0.01, \"Should use current temperature\")\n\t\tassert.InDelta(t, 1600.0, result2[\"0\"].MemoryUsed, 0.01, \"Should use current memory\")\n\t\tassert.EqualValues(t, 2, gm.GpuDataMap[\"0\"].Count, \"Count should still be 2\")\n\t})\n\n\tt.Run(\"tracks separate averages per cache key\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"TestGPU\",\n\t\t\t\t\tTemperature: 55.0,\n\t\t\t\t\tMemoryUsed:  1500,\n\t\t\t\t\tMemoryTotal: 8000,\n\t\t\t\t\tUsage:       100, // Initial: 100 over 2 counts = 50 avg\n\t\t\t\t\tPower:       200, // Initial: 200 over 2 counts = 100 avg\n\t\t\t\t\tCount:       2,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tcacheKey1 := uint16(5000)\n\t\tcacheKey2 := uint16(10000)\n\n\t\t// First check with cacheKey1 - baseline\n\t\tresult1 := gm.GetCurrentData(cacheKey1)\n\t\tassert.InDelta(t, 50.0, result1[\"0\"].Usage, 0.01, \"CacheKey1: Initial average should be 50\")\n\t\tassert.InDelta(t, 100.0, result1[\"0\"].Power, 0.01, \"CacheKey1: Initial average should be 100\")\n\n\t\t// Simulate GPU activity - accumulate more data\n\t\tgm.GpuDataMap[\"0\"].Usage += 60  // Now total: 160\n\t\tgm.GpuDataMap[\"0\"].Power += 150 // Now total: 350\n\t\tgm.GpuDataMap[\"0\"].Count += 3   // Now total: 5\n\n\t\t// Check with cacheKey1 again - should get delta since last cacheKey1 check\n\t\tresult2 := gm.GetCurrentData(cacheKey1)\n\t\tassert.InDelta(t, 20.0, result2[\"0\"].Usage, 0.01, \"CacheKey1: Delta average should be 60/3 = 20\")\n\t\tassert.InDelta(t, 50.0, result2[\"0\"].Power, 0.01, \"CacheKey1: Delta average should be 150/3 = 50\")\n\n\t\t// Check with cacheKey2 for the first time - should get average since beginning\n\t\tresult3 := gm.GetCurrentData(cacheKey2)\n\t\tassert.InDelta(t, 32.0, result3[\"0\"].Usage, 0.01, \"CacheKey2: Total average should be 160/5 = 32\")\n\t\tassert.InDelta(t, 70.0, result3[\"0\"].Power, 0.01, \"CacheKey2: Total average should be 350/5 = 70\")\n\n\t\t// Simulate more GPU activity\n\t\tgm.GpuDataMap[\"0\"].Usage += 80  // Now total: 240\n\t\tgm.GpuDataMap[\"0\"].Power += 160 // Now total: 510\n\t\tgm.GpuDataMap[\"0\"].Count += 2   // Now total: 7\n\n\t\t// Check with cacheKey1 - should get delta since last cacheKey1 check\n\t\tresult4 := gm.GetCurrentData(cacheKey1)\n\t\tassert.InDelta(t, 40.0, result4[\"0\"].Usage, 0.01, \"CacheKey1: New delta average should be 80/2 = 40\")\n\t\tassert.InDelta(t, 80.0, result4[\"0\"].Power, 0.01, \"CacheKey1: New delta average should be 160/2 = 80\")\n\n\t\t// Check with cacheKey2 - should get delta since last cacheKey2 check\n\t\tresult5 := gm.GetCurrentData(cacheKey2)\n\t\tassert.InDelta(t, 40.0, result5[\"0\"].Usage, 0.01, \"CacheKey2: Delta average should be 80/2 = 40\")\n\t\tassert.InDelta(t, 80.0, result5[\"0\"].Power, 0.01, \"CacheKey2: Delta average should be 160/2 = 80\")\n\n\t\t// Verify snapshots exist for both cache keys\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey1])\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey2])\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey1][\"0\"])\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey2][\"0\"])\n\t})\n}\n\nfunc TestCalculateDeltaCount(t *testing.T) {\n\tgm := &GPUManager{}\n\n\tt.Run(\"with no previous snapshot\", func(t *testing.T) {\n\t\tdelta := gm.calculateDeltaCount(10, nil)\n\t\tassert.Equal(t, uint32(10), delta, \"Should return current count when no snapshot exists\")\n\t})\n\n\tt.Run(\"with previous snapshot\", func(t *testing.T) {\n\t\tsnapshot := &gpuSnapshot{count: 5}\n\t\tdelta := gm.calculateDeltaCount(15, snapshot)\n\t\tassert.Equal(t, uint32(10), delta, \"Should return difference between current and snapshot\")\n\t})\n\n\tt.Run(\"with same count\", func(t *testing.T) {\n\t\tsnapshot := &gpuSnapshot{count: 10}\n\t\tdelta := gm.calculateDeltaCount(10, snapshot)\n\t\tassert.Equal(t, uint32(0), delta, \"Should return zero when count hasn't changed\")\n\t})\n}\n\nfunc TestCalculateDeltas(t *testing.T) {\n\tgm := &GPUManager{}\n\n\tt.Run(\"with no previous snapshot\", func(t *testing.T) {\n\t\tgpu := &system.GPUData{\n\t\t\tUsage:    100.5,\n\t\t\tPower:    250.75,\n\t\t\tPowerPkg: 300.25,\n\t\t}\n\t\tdeltaUsage, deltaPower, deltaPowerPkg := gm.calculateDeltas(gpu, nil)\n\t\tassert.Equal(t, 100.5, deltaUsage)\n\t\tassert.Equal(t, 250.75, deltaPower)\n\t\tassert.Equal(t, 300.25, deltaPowerPkg)\n\t})\n\n\tt.Run(\"with previous snapshot\", func(t *testing.T) {\n\t\tgpu := &system.GPUData{\n\t\t\tUsage:    150.5,\n\t\t\tPower:    300.75,\n\t\t\tPowerPkg: 400.25,\n\t\t}\n\t\tsnapshot := &gpuSnapshot{\n\t\t\tusage:    100.5,\n\t\t\tpower:    250.75,\n\t\t\tpowerPkg: 300.25,\n\t\t}\n\t\tdeltaUsage, deltaPower, deltaPowerPkg := gm.calculateDeltas(gpu, snapshot)\n\t\tassert.InDelta(t, 50.0, deltaUsage, 0.01)\n\t\tassert.InDelta(t, 50.0, deltaPower, 0.01)\n\t\tassert.InDelta(t, 100.0, deltaPowerPkg, 0.01)\n\t})\n}\n\nfunc TestCalculateIntelGPUUsage(t *testing.T) {\n\tgm := &GPUManager{}\n\n\tt.Run(\"with no previous snapshot\", func(t *testing.T) {\n\t\tgpuAvg := &system.GPUData{\n\t\t\tEngines: make(map[string]float64),\n\t\t}\n\t\tgpu := &system.GPUData{\n\t\t\tEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 80.0,\n\t\t\t\t\"Video\":     40.0,\n\t\t\t\t\"Compute\":   60.0,\n\t\t\t},\n\t\t}\n\t\tmaxUsage := gm.calculateIntelGPUUsage(gpuAvg, gpu, nil, 2)\n\n\t\tassert.Equal(t, 40.0, maxUsage, \"Should return max engine usage (80/2=40)\")\n\t\tassert.Equal(t, 40.0, gpuAvg.Engines[\"Render/3D\"])\n\t\tassert.Equal(t, 20.0, gpuAvg.Engines[\"Video\"])\n\t\tassert.Equal(t, 30.0, gpuAvg.Engines[\"Compute\"])\n\t})\n\n\tt.Run(\"with previous snapshot\", func(t *testing.T) {\n\t\tgpuAvg := &system.GPUData{\n\t\t\tEngines: make(map[string]float64),\n\t\t}\n\t\tgpu := &system.GPUData{\n\t\t\tEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 180.0,\n\t\t\t\t\"Video\":     100.0,\n\t\t\t\t\"Compute\":   140.0,\n\t\t\t},\n\t\t}\n\t\tsnapshot := &gpuSnapshot{\n\t\t\tengines: map[string]float64{\n\t\t\t\t\"Render/3D\": 80.0,\n\t\t\t\t\"Video\":     40.0,\n\t\t\t\t\"Compute\":   60.0,\n\t\t\t},\n\t\t}\n\t\tmaxUsage := gm.calculateIntelGPUUsage(gpuAvg, gpu, snapshot, 5)\n\n\t\t// Deltas: Render/3D=100, Video=60, Compute=80 over 5 counts\n\t\tassert.Equal(t, 20.0, maxUsage, \"Should return max engine delta (100/5=20)\")\n\t\tassert.Equal(t, 20.0, gpuAvg.Engines[\"Render/3D\"])\n\t\tassert.Equal(t, 12.0, gpuAvg.Engines[\"Video\"])\n\t\tassert.Equal(t, 16.0, gpuAvg.Engines[\"Compute\"])\n\t})\n\n\tt.Run(\"handles missing engine in snapshot\", func(t *testing.T) {\n\t\tgpuAvg := &system.GPUData{\n\t\t\tEngines: make(map[string]float64),\n\t\t}\n\t\tgpu := &system.GPUData{\n\t\t\tEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 100.0,\n\t\t\t\t\"NewEngine\": 50.0,\n\t\t\t},\n\t\t}\n\t\tsnapshot := &gpuSnapshot{\n\t\t\tengines: map[string]float64{\n\t\t\t\t\"Render/3D\": 80.0,\n\t\t\t\t// NewEngine doesn't exist in snapshot\n\t\t\t},\n\t\t}\n\t\tmaxUsage := gm.calculateIntelGPUUsage(gpuAvg, gpu, snapshot, 2)\n\n\t\tassert.Equal(t, 25.0, maxUsage)\n\t\tassert.Equal(t, 10.0, gpuAvg.Engines[\"Render/3D\"], \"Should use delta for existing engine\")\n\t\tassert.Equal(t, 25.0, gpuAvg.Engines[\"NewEngine\"], \"Should use full value for new engine\")\n\t})\n}\n\nfunc TestUpdateInstantaneousValues(t *testing.T) {\n\tgm := &GPUManager{}\n\n\tt.Run(\"updates temperature, memory used and total\", func(t *testing.T) {\n\t\tgpuAvg := &system.GPUData{\n\t\t\tTemperature: 50.123,\n\t\t\tMemoryUsed:  1000.456,\n\t\t\tMemoryTotal: 8000.789,\n\t\t}\n\t\tgpu := &system.GPUData{\n\t\t\tTemperature: 75.567,\n\t\t\tMemoryUsed:  2500.891,\n\t\t\tMemoryTotal: 8192.234,\n\t\t}\n\n\t\tgm.updateInstantaneousValues(gpuAvg, gpu)\n\n\t\tassert.Equal(t, 75.57, gpuAvg.Temperature, \"Should update and round temperature\")\n\t\tassert.Equal(t, 2500.89, gpuAvg.MemoryUsed, \"Should update and round memory used\")\n\t\tassert.Equal(t, 8192.23, gpuAvg.MemoryTotal, \"Should update and round memory total\")\n\t})\n}\n\nfunc TestStoreSnapshot(t *testing.T) {\n\tgm := &GPUManager{\n\t\tlastSnapshots: make(map[uint16]map[string]*gpuSnapshot),\n\t}\n\n\tt.Run(\"stores standard GPU snapshot\", func(t *testing.T) {\n\t\tcacheKey := uint16(5000)\n\t\tgm.lastSnapshots[cacheKey] = make(map[string]*gpuSnapshot)\n\n\t\tgpu := &system.GPUData{\n\t\t\tCount:    10.0,\n\t\t\tUsage:    150.5,\n\t\t\tPower:    250.75,\n\t\t\tPowerPkg: 300.25,\n\t\t}\n\n\t\tgm.storeSnapshot(\"0\", gpu, cacheKey)\n\n\t\tsnapshot := gm.lastSnapshots[cacheKey][\"0\"]\n\t\tassert.NotNil(t, snapshot)\n\t\tassert.Equal(t, uint32(10), snapshot.count)\n\t\tassert.Equal(t, 150.5, snapshot.usage)\n\t\tassert.Equal(t, 250.75, snapshot.power)\n\t\tassert.Equal(t, 300.25, snapshot.powerPkg)\n\t\tassert.Nil(t, snapshot.engines, \"Should not have engines for standard GPU\")\n\t})\n\n\tt.Run(\"stores Intel GPU snapshot with engines\", func(t *testing.T) {\n\t\tcacheKey := uint16(10000)\n\t\tgm.lastSnapshots[cacheKey] = make(map[string]*gpuSnapshot)\n\n\t\tgpu := &system.GPUData{\n\t\t\tCount:    5.0,\n\t\t\tUsage:    100.0,\n\t\t\tPower:    200.0,\n\t\t\tPowerPkg: 250.0,\n\t\t\tEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 80.0,\n\t\t\t\t\"Video\":     40.0,\n\t\t\t},\n\t\t}\n\n\t\tgm.storeSnapshot(\"0\", gpu, cacheKey)\n\n\t\tsnapshot := gm.lastSnapshots[cacheKey][\"0\"]\n\t\tassert.NotNil(t, snapshot)\n\t\tassert.Equal(t, uint32(5), snapshot.count)\n\t\tassert.NotNil(t, snapshot.engines, \"Should have engines for Intel GPU\")\n\t\tassert.Equal(t, 80.0, snapshot.engines[\"Render/3D\"])\n\t\tassert.Equal(t, 40.0, snapshot.engines[\"Video\"])\n\t\tassert.Len(t, snapshot.engines, 2)\n\t})\n\n\tt.Run(\"overwrites existing snapshot\", func(t *testing.T) {\n\t\tcacheKey := uint16(5000)\n\t\tgm.lastSnapshots[cacheKey] = make(map[string]*gpuSnapshot)\n\n\t\t// Store initial snapshot\n\t\tgpu1 := &system.GPUData{Count: 5.0, Usage: 100.0, Power: 200.0}\n\t\tgm.storeSnapshot(\"0\", gpu1, cacheKey)\n\n\t\t// Store updated snapshot\n\t\tgpu2 := &system.GPUData{Count: 10.0, Usage: 250.0, Power: 400.0}\n\t\tgm.storeSnapshot(\"0\", gpu2, cacheKey)\n\n\t\tsnapshot := gm.lastSnapshots[cacheKey][\"0\"]\n\t\tassert.Equal(t, uint32(10), snapshot.count, \"Should overwrite previous count\")\n\t\tassert.Equal(t, 250.0, snapshot.usage, \"Should overwrite previous usage\")\n\t\tassert.Equal(t, 400.0, snapshot.power, \"Should overwrite previous power\")\n\t})\n}\n\nfunc TestCountGPUNames(t *testing.T) {\n\tt.Run(\"returns empty map for no GPUs\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t\t}\n\t\tcounts := gm.countGPUNames()\n\t\tassert.Empty(t, counts)\n\t})\n\n\tt.Run(\"counts unique GPU names\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {Name: \"GPU A\"},\n\t\t\t\t\"1\": {Name: \"GPU B\"},\n\t\t\t\t\"2\": {Name: \"GPU C\"},\n\t\t\t},\n\t\t}\n\t\tcounts := gm.countGPUNames()\n\t\tassert.Equal(t, 1, counts[\"GPU A\"])\n\t\tassert.Equal(t, 1, counts[\"GPU B\"])\n\t\tassert.Equal(t, 1, counts[\"GPU C\"])\n\t\tassert.Len(t, counts, 3)\n\t})\n\n\tt.Run(\"counts duplicate GPU names\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\"0\": {Name: \"RTX 4090\"},\n\t\t\t\t\"1\": {Name: \"RTX 4090\"},\n\t\t\t\t\"2\": {Name: \"RTX 4090\"},\n\t\t\t\t\"3\": {Name: \"RTX 3080\"},\n\t\t\t},\n\t\t}\n\t\tcounts := gm.countGPUNames()\n\t\tassert.Equal(t, 3, counts[\"RTX 4090\"])\n\t\tassert.Equal(t, 1, counts[\"RTX 3080\"])\n\t\tassert.Len(t, counts, 2)\n\t})\n}\n\nfunc TestInitializeSnapshots(t *testing.T) {\n\tt.Run(\"initializes all maps from scratch\", func(t *testing.T) {\n\t\tgm := &GPUManager{}\n\t\tcacheKey := uint16(5000)\n\n\t\tgm.initializeSnapshots(cacheKey)\n\n\t\tassert.NotNil(t, gm.lastAvgData)\n\t\tassert.NotNil(t, gm.lastSnapshots)\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey])\n\t})\n\n\tt.Run(\"initializes only missing maps\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastAvgData: make(map[string]system.GPUData),\n\t\t}\n\t\tcacheKey := uint16(5000)\n\n\t\tgm.initializeSnapshots(cacheKey)\n\n\t\tassert.NotNil(t, gm.lastAvgData, \"Should preserve existing lastAvgData\")\n\t\tassert.NotNil(t, gm.lastSnapshots)\n\t\tassert.NotNil(t, gm.lastSnapshots[cacheKey])\n\t})\n\n\tt.Run(\"adds new cache key to existing snapshots\", func(t *testing.T) {\n\t\texistingKey := uint16(5000)\n\t\tnewKey := uint16(10000)\n\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\texistingKey: {\"0\": {count: 10}},\n\t\t\t},\n\t\t}\n\n\t\tgm.initializeSnapshots(newKey)\n\n\t\tassert.NotNil(t, gm.lastSnapshots[existingKey], \"Should preserve existing cache key\")\n\t\tassert.NotNil(t, gm.lastSnapshots[newKey], \"Should add new cache key\")\n\t\tassert.NotNil(t, gm.lastSnapshots[existingKey][\"0\"], \"Should preserve existing snapshot data\")\n\t})\n}\n\nfunc TestCalculateGPUAverage(t *testing.T) {\n\tt.Run(\"returns cached average when deltaCount is zero\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {\n\t\t\t\t\t\"0\": {count: 10, usage: 100, power: 200},\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastAvgData: map[string]system.GPUData{\n\t\t\t\t\"0\": {Usage: 50.0, Power: 100.0},\n\t\t\t},\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tCount:       10.0, // Same as snapshot, so delta = 0\n\t\t\tUsage:       100.0,\n\t\t\tPower:       200.0,\n\t\t\tTemperature: 50.0, // Non-zero to avoid \"suspended\" check\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, 50.0, result.Usage, \"Should return cached average\")\n\t\tassert.Equal(t, 100.0, result.Power, \"Should return cached average\")\n\t})\n\n\tt.Run(\"returns zero value when GPU is suspended\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {\n\t\t\t\t\t\"0\": {count: 10, usage: 100, power: 200},\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastAvgData: map[string]system.GPUData{\n\t\t\t\t\"0\": {Usage: 50.0, Power: 100.0},\n\t\t\t},\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tName:        \"Test GPU\",\n\t\t\tCount:       10.0,\n\t\t\tTemperature: 0,\n\t\t\tMemoryUsed:  0,\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, 0.0, result.Usage, \"Should return zero usage\")\n\t\tassert.Equal(t, 0.0, result.Power, \"Should return zero power\")\n\t})\n\n\tt.Run(\"calculates average for standard GPU\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {},\n\t\t\t},\n\t\t\tlastAvgData: make(map[string]system.GPUData),\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tName:  \"Test GPU\",\n\t\t\tCount: 4.0,\n\t\t\tUsage: 200.0, // 200 / 4 = 50\n\t\t\tPower: 400.0, // 400 / 4 = 100\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, 50.0, result.Usage)\n\t\tassert.Equal(t, 100.0, result.Power)\n\t\tassert.Equal(t, \"Test GPU\", result.Name)\n\t})\n\n\tt.Run(\"calculates average for Intel GPU with engines\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {},\n\t\t\t},\n\t\t\tlastAvgData: make(map[string]system.GPUData),\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tName:     \"Intel GPU\",\n\t\t\tCount:    5.0,\n\t\t\tPower:    500.0,\n\t\t\tPowerPkg: 600.0,\n\t\t\tEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 100.0, // 100 / 5 = 20\n\t\t\t\t\"Video\":     50.0,  // 50 / 5 = 10\n\t\t\t},\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, 100.0, result.Power)\n\t\tassert.Equal(t, 120.0, result.PowerPkg)\n\t\tassert.Equal(t, 20.0, result.Usage, \"Should use max engine usage\")\n\t\tassert.Equal(t, 20.0, result.Engines[\"Render/3D\"])\n\t\tassert.Equal(t, 10.0, result.Engines[\"Video\"])\n\t})\n\n\tt.Run(\"calculates delta from previous snapshot\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {\n\t\t\t\t\t\"0\": {\n\t\t\t\t\t\tcount:    2,\n\t\t\t\t\t\tusage:    50.0,\n\t\t\t\t\t\tpower:    100.0,\n\t\t\t\t\t\tpowerPkg: 120.0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tlastAvgData: make(map[string]system.GPUData),\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tName:     \"Test GPU\",\n\t\t\tCount:    7.0,   // Delta = 7 - 2 = 5\n\t\t\tUsage:    200.0, // Delta = 200 - 50 = 150, avg = 150/5 = 30\n\t\t\tPower:    350.0, // Delta = 350 - 100 = 250, avg = 250/5 = 50\n\t\t\tPowerPkg: 420.0, // Delta = 420 - 120 = 300, avg = 300/5 = 60\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, 30.0, result.Usage)\n\t\tassert.Equal(t, 50.0, result.Power)\n\t})\n\n\tt.Run(\"stores result in lastAvgData\", func(t *testing.T) {\n\t\tgm := &GPUManager{\n\t\t\tlastSnapshots: map[uint16]map[string]*gpuSnapshot{\n\t\t\t\t5000: {},\n\t\t\t},\n\t\t\tlastAvgData: make(map[string]system.GPUData),\n\t\t}\n\n\t\tgpu := &system.GPUData{\n\t\t\tCount: 2.0,\n\t\t\tUsage: 100.0,\n\t\t\tPower: 200.0,\n\t\t}\n\n\t\tresult := gm.calculateGPUAverage(\"0\", gpu, 5000)\n\n\t\tassert.Equal(t, result, gm.lastAvgData[\"0\"], \"Should store calculated average\")\n\t})\n}\n\nfunc TestGPUCapabilitiesAndLegacyPriority(t *testing.T) {\n\t// Save original PATH\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\thasAmdSysfs := (&GPUManager{}).hasAmdSysfs()\n\n\ttests := []struct {\n\t\tname           string\n\t\tsetupCommands  func(string) error\n\t\twantNvidiaSmi  bool\n\t\twantRocmSmi    bool\n\t\twantTegrastats bool\n\t\twantNvtop      bool\n\t\twantErr        bool\n\t}{\n\t\t{\n\t\t\tname: \"nvidia-smi not available\",\n\t\t\tsetupCommands: func(_ string) error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantNvidiaSmi:  false,\n\t\t\twantRocmSmi:    false,\n\t\t\twantTegrastats: false,\n\t\t\twantNvtop:      false,\n\t\t\twantErr:        true,\n\t\t},\n\t\t{\n\t\t\tname: \"nvidia-smi available\",\n\t\t\tsetupCommands: func(tempDir string) error {\n\t\t\t\tpath := filepath.Join(tempDir, \"nvidia-smi\")\n\t\t\t\tscript := `#!/bin/sh\necho \"test\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantNvidiaSmi:  true,\n\t\t\twantTegrastats: false,\n\t\t\twantRocmSmi:    false,\n\t\t\twantNvtop:      false,\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tname: \"rocm-smi available\",\n\t\t\tsetupCommands: func(tempDir string) error {\n\t\t\t\tpath := filepath.Join(tempDir, \"rocm-smi\")\n\t\t\t\tscript := `#!/bin/sh\necho \"test\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantNvidiaSmi:  false,\n\t\t\twantRocmSmi:    true,\n\t\t\twantTegrastats: false,\n\t\t\twantNvtop:      false,\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tname: \"tegrastats available\",\n\t\t\tsetupCommands: func(tempDir string) error {\n\t\t\t\tpath := filepath.Join(tempDir, \"tegrastats\")\n\t\t\t\tscript := `#!/bin/sh\necho \"test\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantNvidiaSmi:  false,\n\t\t\twantRocmSmi:    false,\n\t\t\twantTegrastats: true,\n\t\t\twantNvtop:      false,\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tname: \"nvtop available\",\n\t\t\tsetupCommands: func(tempDir string) error {\n\t\t\t\tpath := filepath.Join(tempDir, \"nvtop\")\n\t\t\t\tscript := `#!/bin/sh\necho \"[]\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantNvidiaSmi:  false,\n\t\t\twantRocmSmi:    false,\n\t\t\twantTegrastats: false,\n\t\t\twantNvtop:      true,\n\t\t\twantErr:        false,\n\t\t},\n\t\t{\n\t\t\tname: \"no gpu tools available\",\n\t\t\tsetupCommands: func(_ string) error {\n\t\t\t\tos.Setenv(\"PATH\", \"\")\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttempDir := t.TempDir()\n\t\t\tos.Setenv(\"PATH\", tempDir)\n\t\t\tif err := tt.setupCommands(tempDir); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tgm := &GPUManager{}\n\t\t\tcaps := gm.discoverGpuCapabilities()\n\t\t\tvar err error\n\t\t\tif !hasAnyGpuCollector(caps) {\n\t\t\t\terr = fmt.Errorf(noGPUFoundMsg)\n\t\t\t}\n\t\t\tpriorities := gm.resolveLegacyCollectorPriority(caps)\n\t\t\thasPriority := func(source collectorSource) bool {\n\t\t\t\tfor _, s := range priorities {\n\t\t\t\t\tif s == source {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tgotNvidiaSmi := hasPriority(collectorSourceNvidiaSMI)\n\t\t\tgotRocmSmi := hasPriority(collectorSourceRocmSMI)\n\t\t\tgotTegrastats := caps.hasTegrastats\n\t\t\tgotNvtop := caps.hasNvtop\n\n\t\t\tt.Logf(\"nvidiaSmi: %v, rocmSmi: %v, tegrastats: %v\", gotNvidiaSmi, gotRocmSmi, gotTegrastats)\n\n\t\t\twantErr := tt.wantErr\n\t\t\tif hasAmdSysfs && (tt.name == \"nvidia-smi not available\" || tt.name == \"no gpu tools available\") {\n\t\t\t\twantErr = false\n\t\t\t}\n\t\t\tif wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.wantNvidiaSmi, gotNvidiaSmi)\n\t\t\tassert.Equal(t, tt.wantRocmSmi, gotRocmSmi)\n\t\t\tassert.Equal(t, tt.wantTegrastats, gotTegrastats)\n\t\t\tassert.Equal(t, tt.wantNvtop, gotNvtop)\n\t\t})\n\t}\n}\n\nfunc TestCollectorStartHelpers(t *testing.T) {\n\t// Save original PATH\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\t// Set up temp dir with the commands\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcommand  string\n\t\tsetup    func(t *testing.T) error\n\t\tvalidate func(t *testing.T, gm *GPUManager)\n\t\tgm       *GPUManager\n\t}{\n\t\t{\n\t\t\tname:    \"nvidia-smi collector\",\n\t\t\tcommand: \"nvidia-smi\",\n\t\t\tsetup: func(t *testing.T) error {\n\t\t\t\tpath := filepath.Join(dir, \"nvidia-smi\")\n\t\t\t\tscript := `#!/bin/sh\necho \"0, NVIDIA Test GPU, 50, 1024, 4096, 25, 100\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, gm *GPUManager) {\n\t\t\t\tgpu, exists := gm.GpuDataMap[\"0\"]\n\t\t\t\tassert.True(t, exists)\n\t\t\t\tif exists {\n\t\t\t\t\tassert.Equal(t, \"Test GPU\", gpu.Name)\n\t\t\t\t\tassert.Equal(t, 50.0, gpu.Temperature)\n\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"rocm-smi collector\",\n\t\t\tcommand: \"rocm-smi\",\n\t\t\tsetup: func(t *testing.T) error {\n\t\t\t\tpath := filepath.Join(dir, \"rocm-smi\")\n\t\t\t\tscript := `#!/bin/sh\necho '{\"card0\": {\"Temperature (Sensor edge) (C)\": \"49.0\", \"Current Socket Graphics Package Power (W)\": \"28.159\", \"GPU use (%)\": \"0\", \"VRAM Total Memory (B)\": \"536870912\", \"VRAM Total Used Memory (B)\": \"445550592\", \"Card Series\": \"Rembrandt [Radeon 680M]\", \"Card Model\": \"0x1681\", \"Card Vendor\": \"Advanced Micro Devices, Inc. [AMD/ATI]\", \"Card SKU\": \"REMBRANDT\", \"Subsystem ID\": \"0x8a22\", \"Device Rev\": \"0xc8\", \"Node ID\": \"1\", \"GUID\": \"34756\", \"GFX Version\": \"gfx1035\"}}'`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, gm *GPUManager) {\n\t\t\t\tgpu, exists := gm.GpuDataMap[\"34756\"]\n\t\t\t\tassert.True(t, exists)\n\t\t\t\tif exists {\n\t\t\t\t\tassert.Equal(t, \"Rembrandt [Radeon 680M]\", gpu.Name)\n\t\t\t\t\tassert.InDelta(t, 49.0, gpu.Temperature, 0.01)\n\t\t\t\t\tassert.InDelta(t, 28.159, gpu.Power, 0.01)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"tegrastats collector\",\n\t\t\tcommand: \"tegrastats\",\n\t\t\tsetup: func(t *testing.T) error {\n\t\t\t\tpath := filepath.Join(dir, \"tegrastats\")\n\t\t\t\tscript := `#!/bin/sh\necho \"11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000mW\"`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, gm *GPUManager) {\n\t\t\t\tgpu, exists := gm.GpuDataMap[\"0\"]\n\t\t\t\tassert.True(t, exists)\n\t\t\t\tif exists {\n\t\t\t\t\tassert.InDelta(t, 70.0, gpu.Temperature, 0.1)\n\t\t\t\t}\n\t\t\t},\n\t\t\tgm: &GPUManager{\n\t\t\t\tGpuDataMap: map[string]*system.GPUData{\n\t\t\t\t\t\"0\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"nvtop collector\",\n\t\t\tcommand: \"nvtop\",\n\t\t\tsetup: func(t *testing.T) error {\n\t\t\t\tpath := filepath.Join(dir, \"nvtop\")\n\t\t\t\tscript := `#!/bin/sh\necho '[{\"device_name\":\"NVIDIA Test GPU\",\"temp\":\"52C\",\"power_draw\":\"31W\",\"gpu_util\":\"37%\",\"mem_total\":\"4294967296\",\"mem_used\":\"536870912\",\"processes\":[]}]'`\n\t\t\t\tif err := os.WriteFile(path, []byte(script), 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tvalidate: func(t *testing.T, gm *GPUManager) {\n\t\t\t\tgpu, exists := gm.GpuDataMap[\"n0\"]\n\t\t\t\tassert.True(t, exists)\n\t\t\t\tif exists {\n\t\t\t\t\tassert.Equal(t, \"NVIDIA Test GPU\", gpu.Name)\n\t\t\t\t\tassert.Equal(t, 52.0, gpu.Temperature)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.setup(t); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif tt.gm == nil {\n\t\t\t\ttt.gm = &GPUManager{\n\t\t\t\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t\t\t\t}\n\t\t\t}\n\t\t\tswitch tt.command {\n\t\t\tcase nvidiaSmiCmd:\n\t\t\t\ttt.gm.startNvidiaSmiCollector(\"4\")\n\t\t\tcase rocmSmiCmd:\n\t\t\t\ttt.gm.startRocmSmiCollector(4300 * time.Millisecond)\n\t\t\tcase tegraStatsCmd:\n\t\t\t\ttt.gm.startTegraStatsCollector(\"3700\")\n\t\t\tcase nvtopCmd:\n\t\t\t\ttt.gm.startNvtopCollector(\"30\", nil)\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unknown test command %q\", tt.command)\n\t\t\t}\n\t\t\ttime.Sleep(50 * time.Millisecond) // Give collector time to run\n\t\t\ttt.validate(t, tt.gm)\n\t\t})\n\t}\n}\n\nfunc TestNewGPUManagerPriorityNvtopFallback(t *testing.T) {\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"nvtop,nvidia-smi\")\n\n\tnvtopPath := filepath.Join(dir, \"nvtop\")\n\tnvtopScript := `#!/bin/sh\necho 'not-json'`\n\trequire.NoError(t, os.WriteFile(nvtopPath, []byte(nvtopScript), 0755))\n\n\tnvidiaPath := filepath.Join(dir, \"nvidia-smi\")\n\tnvidiaScript := `#!/bin/sh\necho \"0, NVIDIA Priority GPU, 45, 512, 2048, 12, 25\"`\n\trequire.NoError(t, os.WriteFile(nvidiaPath, []byte(nvidiaScript), 0755))\n\n\tgm, err := NewGPUManager()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, gm)\n\n\ttime.Sleep(150 * time.Millisecond)\n\tgpu, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"Priority GPU\", gpu.Name)\n\tassert.Equal(t, 45.0, gpu.Temperature)\n}\n\nfunc TestNewGPUManagerPriorityMixedCollectors(t *testing.T) {\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"intel_gpu_top,rocm-smi\")\n\n\tintelPath := filepath.Join(dir, \"intel_gpu_top\")\n\tintelScript := `#!/bin/sh\necho \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS             VCS\"\necho \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa       %  se  wa\"\necho \"226  223      338  58  2.00  2.69   1820    965   0.00    0   0    0.00   0   0\"\necho \"189  187      412  67  1.80  2.45   1950    823   8.50    2   1    15.00   1   0\"\n`\n\trequire.NoError(t, os.WriteFile(intelPath, []byte(intelScript), 0755))\n\n\trocmPath := filepath.Join(dir, \"rocm-smi\")\n\trocmScript := `#!/bin/sh\necho '{\"card0\": {\"Temperature (Sensor edge) (C)\": \"49.0\", \"Current Socket Graphics Package Power (W)\": \"28.159\", \"GPU use (%)\": \"0\", \"VRAM Total Memory (B)\": \"536870912\", \"VRAM Total Used Memory (B)\": \"445550592\", \"Card Series\": \"Rembrandt [Radeon 680M]\", \"GUID\": \"34756\"}}'\n`\n\trequire.NoError(t, os.WriteFile(rocmPath, []byte(rocmScript), 0755))\n\n\tgm, err := NewGPUManager()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, gm)\n\n\ttime.Sleep(150 * time.Millisecond)\n\t_, intelOk := gm.GpuDataMap[\"i0\"]\n\t_, amdOk := gm.GpuDataMap[\"34756\"]\n\tassert.True(t, intelOk)\n\tassert.True(t, amdOk)\n}\n\nfunc TestNewGPUManagerPriorityNvmlFallbackToNvidiaSmi(t *testing.T) {\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"nvml,nvidia-smi\")\n\n\tnvidiaPath := filepath.Join(dir, \"nvidia-smi\")\n\tnvidiaScript := `#!/bin/sh\necho \"0, NVIDIA Fallback GPU, 41, 256, 1024, 8, 14\"`\n\trequire.NoError(t, os.WriteFile(nvidiaPath, []byte(nvidiaScript), 0755))\n\n\tgm, err := NewGPUManager()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, gm)\n\n\ttime.Sleep(150 * time.Millisecond)\n\tgpu, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"Fallback GPU\", gpu.Name)\n}\n\nfunc TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) {\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\n\tt.Run(\"configured valid collector unavailable\", func(t *testing.T) {\n\t\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"nvidia-smi\")\n\t\tgm, err := NewGPUManager()\n\t\trequire.Nil(t, gm)\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"no configured GPU collectors are available\")\n\t})\n\n\tt.Run(\"configured collector list has only unknown entries\", func(t *testing.T) {\n\t\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"bad,unknown\")\n\t\tgm, err := NewGPUManager()\n\t\trequire.Nil(t, gm)\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"no configured GPU collectors are available\")\n\t})\n}\n\nfunc TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) {\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\tt.Setenv(\"BESZEL_AGENT_GPU_COLLECTOR\", \"nvidia-smi\")\n\n\ttegraPath := filepath.Join(dir, \"tegrastats\")\n\ttegraScript := `#!/bin/sh\necho \"11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 80% tj@70C VDD_GPU_SOC 1000mW\"`\n\trequire.NoError(t, os.WriteFile(tegraPath, []byte(tegraScript), 0755))\n\n\tgm, err := NewGPUManager()\n\trequire.NoError(t, err)\n\trequire.NotNil(t, gm)\n\n\ttime.Sleep(100 * time.Millisecond)\n\tgpu, ok := gm.GpuDataMap[\"0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, \"GPU\", gpu.Name)\n}\n\n// TestAccumulationTableDriven tests the accumulation behavior for all three GPU types\nfunc TestAccumulation(t *testing.T) {\n\ttype expectedGPUValues struct {\n\t\ttemperature float64\n\t\tmemoryUsed  float64\n\t\tmemoryTotal float64\n\t\tusage       float64\n\t\tpower       float64\n\t\tcount       float64\n\t\tavgUsage    float64\n\t\tavgPower    float64\n\t}\n\n\ttests := []struct {\n\t\tname           string\n\t\tinitialGPUData map[string]*system.GPUData\n\t\tdataSamples    [][]byte\n\t\tparser         func(*GPUManager) func([]byte) bool\n\t\texpectedValues map[string]expectedGPUValues\n\t}{\n\t\t{\n\t\t\tname: \"Jetson GPU accumulation\",\n\t\t\tinitialGPUData: map[string]*system.GPUData{\n\t\t\t\t\"0\": {\n\t\t\t\t\tName:        \"Jetson\",\n\t\t\t\t\tTemperature: 0,\n\t\t\t\t\tUsage:       0,\n\t\t\t\t\tPower:       0,\n\t\t\t\t\tCount:       0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdataSamples: [][]byte{\n\t\t\t\t[]byte(\"11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 30% tj@50.5C VDD_GPU_SOC 1000mW\"),\n\t\t\t\t[]byte(\"11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 40% tj@60.5C VDD_GPU_SOC 1200mW\"),\n\t\t\t\t[]byte(\"11-14-2024 22:54:33 RAM 1024/4096MB GR3D_FREQ 50% tj@70.5C VDD_GPU_SOC 1400mW\"),\n\t\t\t},\n\t\t\tparser: func(gm *GPUManager) func([]byte) bool {\n\t\t\t\treturn gm.getJetsonParser()\n\t\t\t},\n\t\t\texpectedValues: map[string]expectedGPUValues{\n\t\t\t\t\"0\": {\n\t\t\t\t\ttemperature: 70.5,  // Last value\n\t\t\t\t\tmemoryUsed:  1024,  // Last value\n\t\t\t\t\tmemoryTotal: 4096,  // Last value\n\t\t\t\t\tusage:       120.0, // Accumulated: 30 + 40 + 50\n\t\t\t\t\tpower:       3.6,   // Accumulated: 1.0 + 1.2 + 1.4\n\t\t\t\t\tcount:       3,\n\t\t\t\t\tavgUsage:    40.0, // 120 / 3\n\t\t\t\t\tavgPower:    1.2,  // 3.6 / 3\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"NVIDIA GPU accumulation\",\n\t\t\tinitialGPUData: map[string]*system.GPUData{\n\t\t\t\t// NVIDIA parser will create the GPU data entries\n\t\t\t},\n\t\t\tdataSamples: [][]byte{\n\t\t\t\t[]byte(\"0, NVIDIA GeForce RTX 3080, 50, 5000, 10000, 30, 200\"),\n\t\t\t\t[]byte(\"0, NVIDIA GeForce RTX 3080, 60, 6000, 10000, 40, 250\"),\n\t\t\t\t[]byte(\"0, NVIDIA GeForce RTX 3080, 70, 7000, 10000, 50, 300\"),\n\t\t\t},\n\t\t\tparser: func(gm *GPUManager) func([]byte) bool {\n\t\t\t\treturn gm.parseNvidiaData\n\t\t\t},\n\t\t\texpectedValues: map[string]expectedGPUValues{\n\t\t\t\t\"0\": {\n\t\t\t\t\ttemperature: 70.0,            // Last value\n\t\t\t\t\tmemoryUsed:  7000.0 / 1.024,  // Last value\n\t\t\t\t\tmemoryTotal: 10000.0 / 1.024, // Last value\n\t\t\t\t\tusage:       120.0,           // Accumulated: 30 + 40 + 50\n\t\t\t\t\tpower:       750.0,           // Accumulated: 200 + 250 + 300\n\t\t\t\t\tcount:       3,\n\t\t\t\t\tavgUsage:    40.0,  // 120 / 3\n\t\t\t\t\tavgPower:    250.0, // 750 / 3\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"AMD GPU accumulation\",\n\t\t\tinitialGPUData: map[string]*system.GPUData{\n\t\t\t\t// AMD parser will create the GPU data entries\n\t\t\t},\n\t\t\tdataSamples: [][]byte{\n\t\t\t\t[]byte(`{\"card0\": {\"GUID\": \"34756\", \"Temperature (Sensor edge) (C)\": \"50.0\", \"Current Socket Graphics Package Power (W)\": \"100.0\", \"GPU use (%)\": \"30\", \"VRAM Total Memory (B)\": \"10737418240\", \"VRAM Total Used Memory (B)\": \"1073741824\", \"Card Series\": \"Radeon RX 6800\"}}`),\n\t\t\t\t[]byte(`{\"card0\": {\"GUID\": \"34756\", \"Temperature (Sensor edge) (C)\": \"60.0\", \"Current Socket Graphics Package Power (W)\": \"150.0\", \"GPU use (%)\": \"40\", \"VRAM Total Memory (B)\": \"10737418240\", \"VRAM Total Used Memory (B)\": \"2147483648\", \"Card Series\": \"Radeon RX 6800\"}}`),\n\t\t\t\t[]byte(`{\"card0\": {\"GUID\": \"34756\", \"Temperature (Sensor edge) (C)\": \"70.0\", \"Current Socket Graphics Package Power (W)\": \"200.0\", \"GPU use (%)\": \"50\", \"VRAM Total Memory (B)\": \"10737418240\", \"VRAM Total Used Memory (B)\": \"3221225472\", \"Card Series\": \"Radeon RX 6800\"}}`),\n\t\t\t},\n\t\t\tparser: func(gm *GPUManager) func([]byte) bool {\n\t\t\t\treturn gm.parseAmdData\n\t\t\t},\n\t\t\texpectedValues: map[string]expectedGPUValues{\n\t\t\t\t\"34756\": {\n\t\t\t\t\ttemperature: 70.0,                          // Last value\n\t\t\t\t\tmemoryUsed:  3221225472.0 / (1024 * 1024),  // Last value\n\t\t\t\t\tmemoryTotal: 10737418240.0 / (1024 * 1024), // Last value\n\t\t\t\t\tusage:       120.0,                         // Accumulated: 30 + 40 + 50\n\t\t\t\t\tpower:       450.0,                         // Accumulated: 100 + 150 + 200\n\t\t\t\t\tcount:       3,\n\t\t\t\t\tavgUsage:    40.0,  // 120 / 3\n\t\t\t\t\tavgPower:    150.0, // 450 / 3\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a new GPUManager for each test\n\t\t\tgm := &GPUManager{\n\t\t\t\tGpuDataMap: tt.initialGPUData,\n\t\t\t}\n\n\t\t\t// Get the parser function\n\t\t\tparser := tt.parser(gm)\n\n\t\t\t// Process each data sample\n\t\t\tfor i, sample := range tt.dataSamples {\n\t\t\t\tvalid := parser(sample)\n\t\t\t\tassert.True(t, valid, \"Sample %d should be valid\", i)\n\t\t\t}\n\n\t\t\t// Check accumulated values\n\t\t\tfor id, expected := range tt.expectedValues {\n\t\t\t\tgpu, exists := gm.GpuDataMap[id]\n\t\t\t\tassert.True(t, exists, \"GPU with ID %s should exist\", id)\n\t\t\t\tif !exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tassert.EqualValues(t, expected.temperature, gpu.Temperature, \"Temperature should match\")\n\t\t\t\tassert.EqualValues(t, expected.memoryUsed, gpu.MemoryUsed, \"Memory used should match\")\n\t\t\t\tassert.EqualValues(t, expected.memoryTotal, gpu.MemoryTotal, \"Memory total should match\")\n\t\t\t\tassert.EqualValues(t, expected.usage, gpu.Usage, \"Usage should match\")\n\t\t\t\tassert.EqualValues(t, expected.power, gpu.Power, \"Power should match\")\n\t\t\t\tassert.Equal(t, expected.count, gpu.Count, \"Count should match\")\n\t\t\t}\n\n\t\t\t// Verify average calculation in GetCurrentData\n\t\t\tcacheKey := uint16(5000)\n\t\t\tresult := gm.GetCurrentData(cacheKey)\n\t\t\tfor id, expected := range tt.expectedValues {\n\t\t\t\tgpu, exists := result[id]\n\t\t\t\tassert.True(t, exists, \"GPU with ID %s should exist in GetCurrentData result\", id)\n\t\t\t\tif !exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tassert.EqualValues(t, expected.temperature, gpu.Temperature, \"Temperature in GetCurrentData should match\")\n\t\t\t\tassert.EqualValues(t, expected.avgUsage, gpu.Usage, \"Average usage in GetCurrentData should match\")\n\t\t\t\tassert.EqualValues(t, expected.avgPower, gpu.Power, \"Average power in GetCurrentData should match\")\n\t\t\t}\n\n\t\t\t// Verify that accumulators in the original map are NOT reset (they keep growing)\n\t\t\tfor id, expected := range tt.expectedValues {\n\t\t\t\tgpu, exists := gm.GpuDataMap[id]\n\t\t\t\tassert.True(t, exists, \"GPU with ID %s should still exist after GetCurrentData\", id)\n\t\t\t\tif !exists {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tassert.EqualValues(t, expected.count, gpu.Count, \"Count should remain at accumulated value for GPU ID %s\", id)\n\t\t\t\tassert.EqualValues(t, expected.usage, gpu.Usage, \"Usage should remain at accumulated value for GPU ID %s\", id)\n\t\t\t\tassert.EqualValues(t, expected.power, gpu.Power, \"Power should remain at accumulated value for GPU ID %s\", id)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIntelUpdateFromStats(t *testing.T) {\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\n\t// First sample with power and two engines\n\tsample1 := intelGpuStats{\n\t\tPowerGPU: 10.5,\n\t\tEngines: map[string]float64{\n\t\t\t\"Render/3D\": 20.0,\n\t\t\t\"Video\":     5.0,\n\t\t},\n\t}\n\n\tok := gm.updateIntelFromStats(&sample1)\n\tassert.True(t, ok)\n\n\tgpu := gm.GpuDataMap[\"i0\"]\n\trequire.NotNil(t, gpu)\n\tassert.Equal(t, \"GPU\", gpu.Name)\n\tassert.EqualValues(t, 10.5, gpu.Power)\n\tassert.EqualValues(t, 20.0, gpu.Engines[\"Render/3D\"])\n\tassert.EqualValues(t, 5.0, gpu.Engines[\"Video\"])\n\tassert.Equal(t, float64(1), gpu.Count)\n\n\t// Second sample with zero power (should not add) and additional engine busy\n\tsample2 := intelGpuStats{\n\t\tPowerGPU: 0.0,\n\t\tEngines: map[string]float64{\n\t\t\t\"Render/3D\": 10.0,\n\t\t\t\"Video\":     2.5,\n\t\t\t\"Blitter\":   1.0,\n\t\t},\n\t}\n\t// zero power should not increment power accumulator\n\n\tok = gm.updateIntelFromStats(&sample2)\n\tassert.True(t, ok)\n\n\tgpu = gm.GpuDataMap[\"i0\"]\n\trequire.NotNil(t, gpu)\n\tassert.EqualValues(t, 10.5, gpu.Power)\n\tassert.EqualValues(t, 30.0, gpu.Engines[\"Render/3D\"]) // 20 + 10\n\tassert.EqualValues(t, 7.5, gpu.Engines[\"Video\"])      // 5 + 2.5\n\tassert.EqualValues(t, 1.0, gpu.Engines[\"Blitter\"])\n\tassert.Equal(t, float64(2), gpu.Count)\n}\n\nfunc TestIntelCollectorStreaming(t *testing.T) {\n\t// Save and override PATH\n\torigPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", origPath)\n\n\tdir := t.TempDir()\n\tos.Setenv(\"PATH\", dir)\n\n\t// Create a fake intel_gpu_top that prints -l format with four samples (first will be skipped) and exits\n\tscriptPath := filepath.Join(dir, \"intel_gpu_top\")\n\tscript := `#!/bin/sh\necho \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS             BCS             VCS\"\necho \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa       %  se  wa       %  se  wa\"\necho \"373  373      224  45  1.50  4.13   2554    714   12.34   0   0    0.00   0   0    5.00   0   0\"\necho \"226  223      338  58  2.00  2.69   1820    965   0.00    0   0    0.00   0   0    0.00   0   0\"\necho \"189  187      412  67  1.80  2.45   1950    823   8.50    2   1    15.00   1   0    22.00  0   1\"\necho \"298  295      278  51  2.20  3.12   1675    942   5.75    1   2    9.50    3   1    12.00  1   0\"`\n\tif err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgm := &GPUManager{\n\t\tGpuDataMap: make(map[string]*system.GPUData),\n\t}\n\n\t// Run the collector once; it should read four samples but skip the first and return\n\tif err := gm.collectIntelStats(); err != nil {\n\t\tt.Fatalf(\"collectIntelStats error: %v\", err)\n\t}\n\n\tgpu := gm.GpuDataMap[\"i0\"]\n\trequire.NotNil(t, gpu)\n\t// Power should be sum of samples 2-4 (first is skipped): 2.0 + 1.8 + 2.2 = 6.0\n\tassert.EqualValues(t, 6.0, gpu.Power)\n\tassert.InDelta(t, 8.26, gpu.PowerPkg, 0.01) // Allow small floating point differences\n\t// Engines aggregated from samples 2-4\n\tassert.EqualValues(t, 14.25, gpu.Engines[\"Render/3D\"]) // 0.00 + 8.50 + 5.75\n\tassert.EqualValues(t, 34.0, gpu.Engines[\"Video\"])      // 0.00 + 22.00 + 12.00\n\tassert.EqualValues(t, 24.5, gpu.Engines[\"Blitter\"])    // 0.00 + 15.00 + 9.50\n\t// Count should be 3 samples (first is skipped)\n\tassert.Equal(t, float64(3), gpu.Count)\n}\n\nfunc TestParseIntelHeaders(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\theader1           string\n\t\theader2           string\n\t\twantEngineNames   []string\n\t\twantFriendlyNames []string\n\t\twantPowerIndex    int\n\t\twantPreEngineCols int\n\t}{\n\t\t{\n\t\t\tname:              \"basic headers with RCS BCS VCS\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS             BCS             VCS\",\n\t\t\theader2:           \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa       %  se  wa       %  se  wa\",\n\t\t\twantEngineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\twantFriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\twantPowerIndex:    4, // \"gpu\" is at index 4\n\t\t\twantPreEngineCols: 8, // 17 total cols - 3*3 = 8\n\t\t},\n\t\t{\n\t\t\tname:              \"basic headers with RCS BCS VCS using index in name\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s           RCS/0           BCS/1           VCS/2\",\n\t\t\theader2:           \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa       %  se  wa       %  se  wa\",\n\t\t\twantEngineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\twantFriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\twantPowerIndex:    4, // \"gpu\" is at index 4\n\t\t\twantPreEngineCols: 8, // 17 total cols - 3*3 = 8\n\t\t},\n\t\t{\n\t\t\tname:              \"headers with only RCS\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS\",\n\t\t\theader2:           \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa\",\n\t\t\twantEngineNames:   []string{\"RCS\"},\n\t\t\twantFriendlyNames: []string{\"Render/3D\"},\n\t\t\twantPowerIndex:    4,\n\t\t\twantPreEngineCols: 8, // 11 total - 3*1 = 8\n\t\t},\n\t\t{\n\t\t\tname:              \"headers with VECS and CCS\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             VECS            CCS\",\n\t\t\theader2:           \" req  act       /s   %   gpu   pkg     rd     wr       %  se  wa     %  se  wa\",\n\t\t\twantEngineNames:   []string{\"VECS\", \"CCS\"},\n\t\t\twantFriendlyNames: []string{\"VideoEnhance\", \"Compute\"},\n\t\t\twantPowerIndex:    4,\n\t\t\twantPreEngineCols: 8, // 14 total - 3*2 = 8\n\t\t},\n\t\t{\n\t\t\tname:              \"no engines\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s\",\n\t\t\theader2:           \" req  act       /s   %   gpu   pkg     rd     wr\",\n\t\t\twantEngineNames:   nil, // no engines found, slices remain nil\n\t\t\twantFriendlyNames: nil,\n\t\t\twantPowerIndex:    -1, // no engines, so no search\n\t\t\twantPreEngineCols: 0,\n\t\t},\n\t\t{\n\t\t\tname:              \"power index not found\",\n\t\t\theader1:           \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS\",\n\t\t\theader2:           \" req  act       /s   %   pkg   cpu     rd     wr       %  se  wa\", // no \"gpu\"\n\t\t\twantEngineNames:   []string{\"RCS\"},\n\t\t\twantFriendlyNames: []string{\"Render/3D\"},\n\t\t\twantPowerIndex:    -1, // \"gpu\" not found\n\t\t\twantPreEngineCols: 8,  // 11 total - 3*1 = 8\n\t\t},\n\t\t{\n\t\t\tname:              \"empty headers\",\n\t\t\theader1:           \"\",\n\t\t\theader2:           \"\",\n\t\t\twantEngineNames:   nil, // empty input, slices remain nil\n\t\t\twantFriendlyNames: nil,\n\t\t\twantPowerIndex:    -1,\n\t\t\twantPreEngineCols: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgm := &GPUManager{}\n\t\t\tengineNames, friendlyNames, powerIndex, preEngineCols := gm.parseIntelHeaders(tt.header1, tt.header2)\n\n\t\t\tassert.Equal(t, tt.wantEngineNames, engineNames)\n\t\t\tassert.Equal(t, tt.wantFriendlyNames, friendlyNames)\n\t\t\tassert.Equal(t, tt.wantPowerIndex, powerIndex)\n\t\t\tassert.Equal(t, tt.wantPreEngineCols, preEngineCols)\n\t\t})\n\t}\n}\n\nfunc TestParseIntelData(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tline          string\n\t\tengineNames   []string\n\t\tfriendlyNames []string\n\t\tpowerIndex    int\n\t\tpreEngineCols int\n\t\twantPowerGPU  float64\n\t\twantEngines   map[string]float64\n\t\twantErr       error\n\t}{\n\t\t{\n\t\t\tname:          \"basic data with power and engines\",\n\t\t\tline:          \"373  373      224  45  1.50  4.13   2554    714   12.34   0   0    0.00   0   0    5.00   0   0\",\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  1.50,\n\t\t\twantEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 12.34,\n\t\t\t\t\"Blitter\":   0.00,\n\t\t\t\t\"Video\":     5.00,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"data with zero power\",\n\t\t\tline:          \"226  223      338  58  0.00  2.69   1820    965   0.00    0   0    0.00   0   0    0.00   0   0\",\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  0.00,\n\t\t\twantEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 0.00,\n\t\t\t\t\"Blitter\":   0.00,\n\t\t\t\t\"Video\":     0.00,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"data with no power index\",\n\t\t\tline:          \"373  373      224  45  1.50  4.13   2554    714   12.34   0   0    0.00   0   0    5.00   0   0\",\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    -1,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  0.0, // no power parsed\n\t\t\twantEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 12.34,\n\t\t\t\t\"Blitter\":   0.00,\n\t\t\t\t\"Video\":     5.00,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"data with insufficient columns\",\n\t\t\tline:          \"373  373      224  45  1.50\", // too few columns\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  0.0,\n\t\t\twantEngines:   nil, // empty sample returned\n\t\t\twantErr:       errNoValidData,\n\t\t},\n\t\t{\n\t\t\tname:          \"empty line\",\n\t\t\tline:          \"\",\n\t\t\tengineNames:   []string{\"RCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  0.0,\n\t\t\twantEngines:   nil,\n\t\t\twantErr:       errNoValidData,\n\t\t},\n\t\t{\n\t\t\tname:          \"data with invalid power value\",\n\t\t\tline:          \"373  373      224  45  N/A  4.13   2554    714   12.34   0   0    0.00   0   0    5.00   0   0\",\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  0.0, // N/A can't be parsed\n\t\t\twantEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 12.34,\n\t\t\t\t\"Blitter\":   0.00,\n\t\t\t\t\"Video\":     5.00,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"data with invalid engine value\",\n\t\t\tline:          \"373  373      224  45  1.50  4.13   2554    714   N/A     0   0    0.00   0   0    5.00   0   0\",\n\t\t\tengineNames:   []string{\"RCS\", \"BCS\", \"VCS\"},\n\t\t\tfriendlyNames: []string{\"Render/3D\", \"Blitter\", \"Video\"},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  1.50,\n\t\t\twantEngines: map[string]float64{\n\t\t\t\t\"Render/3D\": 0.0, // N/A becomes 0\n\t\t\t\t\"Blitter\":   0.00,\n\t\t\t\t\"Video\":     5.00,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"data with no engines\",\n\t\t\tline:          \"373  373      224  45  1.50  4.13   2554    714\",\n\t\t\tengineNames:   []string{},\n\t\t\tfriendlyNames: []string{},\n\t\t\tpowerIndex:    4,\n\t\t\tpreEngineCols: 8,\n\t\t\twantPowerGPU:  1.50,\n\t\t\twantEngines:   nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgm := &GPUManager{}\n\t\t\tsample, err := gm.parseIntelData(tt.line, tt.engineNames, tt.friendlyNames, tt.powerIndex, tt.preEngineCols)\n\t\t\tassert.Equal(t, tt.wantErr, err)\n\n\t\t\tassert.Equal(t, tt.wantPowerGPU, sample.PowerGPU)\n\t\t\tassert.Equal(t, tt.wantEngines, sample.Engines)\n\t\t})\n\t}\n}\n\nfunc TestIntelCollectorDeviceEnv(t *testing.T) {\n\tdir := t.TempDir()\n\tt.Setenv(\"PATH\", dir)\n\n\t// Prepare a file to capture args\n\targsFile := filepath.Join(dir, \"args.txt\")\n\n\t// Create a fake intel_gpu_top that records its arguments and prints minimal valid output\n\tscriptPath := filepath.Join(dir, \"intel_gpu_top\")\n\tscript := fmt.Sprintf(`#!/bin/sh\necho \"$@\" > %s\necho \"Freq MHz      IRQ RC6     Power W     IMC MiB/s             RCS             VCS\"\necho \" req  act       /s   %%   gpu   pkg     rd     wr       %%  se  wa       %%  se  wa\"\necho \"226  223      338  58  2.00  2.69   1820    965   0.00    0   0    0.00   0   0\"\necho \"189  187      412  67  1.80  2.45   1950    823   8.50    2   1    15.00   1   0\"\n`, argsFile)\n\tif err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Set device selector via prefixed env var\n\tt.Setenv(\"BESZEL_AGENT_INTEL_GPU_DEVICE\", \"sriov\")\n\n\tgm := &GPUManager{GpuDataMap: make(map[string]*system.GPUData)}\n\tif err := gm.collectIntelStats(); err != nil {\n\t\tt.Fatalf(\"collectIntelStats error: %v\", err)\n\t}\n\n\t// Verify that -d sriov was passed\n\tdata, err := os.ReadFile(argsFile)\n\tif err != nil {\n\t\tt.Fatalf(\"failed reading args file: %v\", err)\n\t}\n\targsStr := strings.TrimSpace(string(data))\n\trequire.Contains(t, argsStr, \"-d sriov\")\n\trequire.Contains(t, argsStr, \"-s \")\n\trequire.Contains(t, argsStr, \"-l\")\n}\n"
  },
  {
    "path": "agent/handlers.go",
    "content": "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/internal/common\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\n\t\"log/slog\"\n)\n\n// HandlerContext provides context for request handlers\ntype HandlerContext struct {\n\tClient      *WebSocketClient\n\tAgent       *Agent\n\tRequest     *common.HubRequest[cbor.RawMessage]\n\tRequestID   *uint32\n\tHubVerified bool\n\t// SendResponse abstracts how a handler sends responses (WS or SSH)\n\tSendResponse func(data any, requestID *uint32) error\n}\n\n// RequestHandler defines the interface for handling specific websocket request types\ntype RequestHandler interface {\n\t// Handle processes the request and returns an error if unsuccessful\n\tHandle(hctx *HandlerContext) error\n}\n\n// Responder sends handler responses back to the hub (over WS or SSH)\ntype Responder interface {\n\tSendResponse(data any, requestID *uint32) error\n}\n\n// HandlerRegistry manages the mapping between actions and their handlers\ntype HandlerRegistry struct {\n\thandlers map[common.WebSocketAction]RequestHandler\n}\n\n// NewHandlerRegistry creates a new handler registry with default handlers\nfunc NewHandlerRegistry() *HandlerRegistry {\n\tregistry := &HandlerRegistry{\n\t\thandlers: make(map[common.WebSocketAction]RequestHandler),\n\t}\n\n\tregistry.Register(common.GetData, &GetDataHandler{})\n\tregistry.Register(common.CheckFingerprint, &CheckFingerprintHandler{})\n\tregistry.Register(common.GetContainerLogs, &GetContainerLogsHandler{})\n\tregistry.Register(common.GetContainerInfo, &GetContainerInfoHandler{})\n\tregistry.Register(common.GetSmartData, &GetSmartDataHandler{})\n\tregistry.Register(common.GetSystemdInfo, &GetSystemdInfoHandler{})\n\n\treturn registry\n}\n\n// Register registers a handler for a specific action type\nfunc (hr *HandlerRegistry) Register(action common.WebSocketAction, handler RequestHandler) {\n\thr.handlers[action] = handler\n}\n\n// Handle routes the request to the appropriate handler\nfunc (hr *HandlerRegistry) Handle(hctx *HandlerContext) error {\n\thandler, exists := hr.handlers[hctx.Request.Action]\n\tif !exists {\n\t\treturn fmt.Errorf(\"unknown action: %d\", hctx.Request.Action)\n\t}\n\n\t// Check verification requirement - default to requiring verification\n\tif hctx.Request.Action != common.CheckFingerprint && !hctx.HubVerified {\n\t\treturn errors.New(\"hub not verified\")\n\t}\n\n\t// Log handler execution for debugging\n\t// slog.Debug(\"Executing handler\", \"action\", hctx.Request.Action)\n\n\treturn handler.Handle(hctx)\n}\n\n// GetHandler returns the handler for a specific action\nfunc (hr *HandlerRegistry) GetHandler(action common.WebSocketAction) (RequestHandler, bool) {\n\thandler, exists := hr.handlers[action]\n\treturn handler, exists\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// GetDataHandler handles system data requests\ntype GetDataHandler struct{}\n\nfunc (h *GetDataHandler) Handle(hctx *HandlerContext) error {\n\tvar options common.DataRequestOptions\n\t_ = cbor.Unmarshal(hctx.Request.Data, &options)\n\n\tsysStats := hctx.Agent.gatherStats(options)\n\treturn hctx.SendResponse(sysStats, hctx.RequestID)\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// CheckFingerprintHandler handles authentication challenges\ntype CheckFingerprintHandler struct{}\n\nfunc (h *CheckFingerprintHandler) Handle(hctx *HandlerContext) error {\n\treturn hctx.Client.handleAuthChallenge(hctx.Request, hctx.RequestID)\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// GetContainerLogsHandler handles container log requests\ntype GetContainerLogsHandler struct{}\n\nfunc (h *GetContainerLogsHandler) Handle(hctx *HandlerContext) error {\n\tif hctx.Agent.dockerManager == nil {\n\t\treturn hctx.SendResponse(\"\", hctx.RequestID)\n\t}\n\n\tvar req common.ContainerLogsRequest\n\tif err := cbor.Unmarshal(hctx.Request.Data, &req); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\tlogContent, err := hctx.Agent.dockerManager.getLogs(ctx, req.ContainerID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn hctx.SendResponse(logContent, hctx.RequestID)\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// GetContainerInfoHandler handles container info requests\ntype GetContainerInfoHandler struct{}\n\nfunc (h *GetContainerInfoHandler) Handle(hctx *HandlerContext) error {\n\tif hctx.Agent.dockerManager == nil {\n\t\treturn hctx.SendResponse(\"\", hctx.RequestID)\n\t}\n\n\tvar req common.ContainerInfoRequest\n\tif err := cbor.Unmarshal(hctx.Request.Data, &req); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\tinfo, err := hctx.Agent.dockerManager.getContainerInfo(ctx, req.ContainerID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn hctx.SendResponse(string(info), hctx.RequestID)\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// GetSmartDataHandler handles SMART data requests\ntype GetSmartDataHandler struct{}\n\nfunc (h *GetSmartDataHandler) Handle(hctx *HandlerContext) error {\n\tif hctx.Agent.smartManager == nil {\n\t\t// return empty map to indicate no data\n\t\treturn hctx.SendResponse(map[string]smart.SmartData{}, hctx.RequestID)\n\t}\n\tif err := hctx.Agent.smartManager.Refresh(false); err != nil {\n\t\tslog.Debug(\"smart refresh failed\", \"err\", err)\n\t}\n\tdata := hctx.Agent.smartManager.GetCurrentData()\n\treturn hctx.SendResponse(data, hctx.RequestID)\n}\n\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n////////////////////////////////////////////////////////////////////////////\n\n// GetSystemdInfoHandler handles detailed systemd service info requests\ntype GetSystemdInfoHandler struct{}\n\nfunc (h *GetSystemdInfoHandler) Handle(hctx *HandlerContext) error {\n\tif hctx.Agent.systemdManager == nil {\n\t\treturn errors.ErrUnsupported\n\t}\n\n\tvar req common.SystemdInfoRequest\n\tif err := cbor.Unmarshal(hctx.Request.Data, &req); err != nil {\n\t\treturn err\n\t}\n\tif req.ServiceName == \"\" {\n\t\treturn errors.New(\"service name is required\")\n\t}\n\n\tdetails, err := hctx.Agent.systemdManager.getServiceDetails(req.ServiceName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn hctx.SendResponse(details, hctx.RequestID)\n}\n"
  },
  {
    "path": "agent/handlers_test.go",
    "content": "//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/internal/common\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// MockHandler for testing\ntype MockHandler struct {\n\trequiresVerification bool\n\tdescription          string\n\thandleFunc           func(ctx *HandlerContext) error\n}\n\nfunc (m *MockHandler) Handle(ctx *HandlerContext) error {\n\tif m.handleFunc != nil {\n\t\treturn m.handleFunc(ctx)\n\t}\n\treturn nil\n}\n\nfunc (m *MockHandler) RequiresVerification() bool {\n\treturn m.requiresVerification\n}\n\n// TestHandlerRegistry tests the handler registry functionality\nfunc TestHandlerRegistry(t *testing.T) {\n\tt.Run(\"default registration\", func(t *testing.T) {\n\t\tregistry := NewHandlerRegistry()\n\n\t\t// Check default handlers are registered\n\t\tgetDataHandler, exists := registry.GetHandler(common.GetData)\n\t\tassert.True(t, exists)\n\t\tassert.IsType(t, &GetDataHandler{}, getDataHandler)\n\n\t\tfingerprintHandler, exists := registry.GetHandler(common.CheckFingerprint)\n\t\tassert.True(t, exists)\n\t\tassert.IsType(t, &CheckFingerprintHandler{}, fingerprintHandler)\n\t})\n\n\tt.Run(\"custom handler registration\", func(t *testing.T) {\n\t\tregistry := NewHandlerRegistry()\n\t\tmockHandler := &MockHandler{\n\t\t\trequiresVerification: true,\n\t\t\tdescription:          \"Test handler\",\n\t\t}\n\n\t\t// Register a custom handler for a mock action\n\t\tconst mockAction common.WebSocketAction = 99\n\t\tregistry.Register(mockAction, mockHandler)\n\n\t\t// Verify registration\n\t\thandler, exists := registry.GetHandler(mockAction)\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, mockHandler, handler)\n\t})\n\n\tt.Run(\"unknown action\", func(t *testing.T) {\n\t\tregistry := NewHandlerRegistry()\n\t\tctx := &HandlerContext{\n\t\t\tRequest: &common.HubRequest[cbor.RawMessage]{\n\t\t\t\tAction: common.WebSocketAction(255), // Unknown action\n\t\t\t},\n\t\t\tHubVerified: true,\n\t\t}\n\n\t\terr := registry.Handle(ctx)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"unknown action: 255\")\n\t})\n\n\tt.Run(\"verification required\", func(t *testing.T) {\n\t\tregistry := NewHandlerRegistry()\n\t\tctx := &HandlerContext{\n\t\t\tRequest: &common.HubRequest[cbor.RawMessage]{\n\t\t\t\tAction: common.GetData, // Requires verification\n\t\t\t},\n\t\t\tHubVerified: false, // Not verified\n\t\t}\n\n\t\terr := registry.Handle(ctx)\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"hub not verified\")\n\t})\n}\n\n// TestCheckFingerprintHandler tests the CheckFingerprint handler\nfunc TestCheckFingerprintHandler(t *testing.T) {\n\thandler := &CheckFingerprintHandler{}\n\n\tt.Run(\"handle with invalid data\", func(t *testing.T) {\n\t\tclient := &WebSocketClient{}\n\t\tctx := &HandlerContext{\n\t\t\tClient:      client,\n\t\t\tHubVerified: false,\n\t\t\tRequest: &common.HubRequest[cbor.RawMessage]{\n\t\t\t\tAction: common.CheckFingerprint,\n\t\t\t\tData:   cbor.RawMessage{}, // Empty/invalid data\n\t\t\t},\n\t\t}\n\n\t\t// Should fail to decode the fingerprint request\n\t\terr := handler.Handle(ctx)\n\t\tassert.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "agent/health/health.go",
    "content": "// Package health provides functions to check and update the health of the agent.\n// It uses a file in the temp directory to store the timestamp of the last connection attempt.\n// If the timestamp is older than 90 seconds, the agent is considered unhealthy.\n// NB: The agent must be started with the Start() method to be considered healthy.\npackage health\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n)\n\n// healthFile is the path to the health file\nvar healthFile = getHealthFilePath()\n\nfunc getHealthFilePath() string {\n\tfilename := \"beszel_health\"\n\tif runtime.GOOS == \"linux\" {\n\t\tfullPath := filepath.Join(\"/dev/shm\", filename)\n\t\tif err := updateHealthFile(fullPath); err == nil {\n\t\t\treturn fullPath\n\t\t}\n\t}\n\treturn filepath.Join(os.TempDir(), filename)\n}\n\nfunc updateHealthFile(path string) error {\n\tfile, err := os.Create(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn file.Close()\n}\n\n// Check checks if the agent is connected by checking the modification time of the health file\nfunc Check() error {\n\tfileInfo, err := os.Stat(healthFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif time.Since(fileInfo.ModTime()) > 91*time.Second {\n\t\tlog.Println(\"over 90 seconds since last connection\")\n\t\treturn errors.New(\"unhealthy\")\n\t}\n\treturn nil\n}\n\n// Update updates the modification time of the health file\nfunc Update() error {\n\treturn updateHealthFile(healthFile)\n}\n\n// CleanUp removes the health file\nfunc CleanUp() error {\n\treturn os.Remove(healthFile)\n}\n"
  },
  {
    "path": "agent/health/health_test.go",
    "content": "//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.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHealth(t *testing.T) {\n\t// Override healthFile to use a temporary directory for this test.\n\toriginalHealthFile := healthFile\n\ttmpDir := t.TempDir()\n\thealthFile = filepath.Join(tmpDir, \"beszel_health_test\")\n\tdefer func() { healthFile = originalHealthFile }()\n\n\tt.Run(\"check with no health file\", func(t *testing.T) {\n\t\terr := Check()\n\t\trequire.Error(t, err)\n\t\tassert.True(t, os.IsNotExist(err), \"expected a file-not-exist error, but got: %v\", err)\n\t})\n\n\tt.Run(\"update and check\", func(t *testing.T) {\n\t\terr := Update()\n\t\trequire.NoError(t, err, \"Update() failed\")\n\n\t\terr = Check()\n\t\tassert.NoError(t, err, \"Check() failed immediately after Update()\")\n\t})\n\n\t// This test uses synctest to simulate time passing.\n\tt.Run(\"check with simulated time\", func(t *testing.T) {\n\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t// Update the file to set the initial timestamp.\n\t\t\trequire.NoError(t, Update(), \"Update() failed inside synctest\")\n\n\t\t\t// Set the mtime to the current fake time to align the file's timestamp with the simulated clock.\n\t\t\tnow := time.Now()\n\t\t\trequire.NoError(t, os.Chtimes(healthFile, now, now), \"Chtimes failed\")\n\n\t\t\t// Wait a duration less than the threshold.\n\t\t\ttime.Sleep(89 * time.Second)\n\t\t\tsynctest.Wait()\n\n\t\t\t// The check should still pass.\n\t\t\tassert.NoError(t, Check(), \"Check() failed after 89s\")\n\n\t\t\t// Wait for the total duration to exceed the threshold.\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tsynctest.Wait()\n\n\t\t\t// The check should now fail as unhealthy.\n\t\t\terr := Check()\n\t\t\trequire.Error(t, err, \"Check() should have failed after 91s\")\n\t\t\tassert.Equal(t, \"unhealthy\", err.Error(), \"Check() returned wrong error\")\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "agent/lhm/beszel_lhm.cs",
    "content": "using System;\nusing System.Globalization;\nusing LibreHardwareMonitor.Hardware;\n\nclass Program\n{\n  static void Main()\n  {\n    var computer = new Computer\n    {\n      IsCpuEnabled = true,\n      IsGpuEnabled = true,\n      IsMemoryEnabled = true,\n      IsMotherboardEnabled = true,\n      IsStorageEnabled = true,\n      // IsPsuEnabled = true,\n      // IsNetworkEnabled = true,\n    };\n    computer.Open();\n\n    var reader = Console.In;\n    var writer = Console.Out;\n\n    string line;\n    while ((line = reader.ReadLine()) != null)\n    {\n      if (line.Trim().Equals(\"getTemps\", StringComparison.OrdinalIgnoreCase))\n      {\n        foreach (var hw in computer.Hardware)\n        {\n          // process main hardware sensors\n          ProcessSensors(hw, writer);\n\n          // process subhardware sensors\n          foreach (var subhardware in hw.SubHardware)\n          {\n            ProcessSensors(subhardware, writer);\n          }\n        }\n        // send empty line to signal end of sensor data\n        writer.WriteLine();\n        writer.Flush();\n      }\n    }\n\n    computer.Close();\n  }\n\n  static void ProcessSensors(IHardware hardware, System.IO.TextWriter writer)\n  {\n    var updated = false;\n    foreach (var sensor in hardware.Sensors)\n    {\n      var validTemp = sensor.SensorType == SensorType.Temperature && sensor.Value.HasValue;\n      if (!validTemp ||\n          sensor.Name.IndexOf(\"Distance\", StringComparison.OrdinalIgnoreCase) >= 0 ||\n          sensor.Name.IndexOf(\"Limit\", StringComparison.OrdinalIgnoreCase) >= 0 ||\n          sensor.Name.IndexOf(\"Critical\", StringComparison.OrdinalIgnoreCase) >= 0 ||\n          sensor.Name.IndexOf(\"Warning\", StringComparison.OrdinalIgnoreCase) >= 0 ||\n          sensor.Name.IndexOf(\"Resolution\", StringComparison.OrdinalIgnoreCase) >= 0)\n      {\n        continue;\n      }\n\n      if (!updated)\n      {\n        hardware.Update();\n        updated = true;\n      }\n\n      var name = sensor.Name;\n      // if sensor.Name starts with \"Temperature\" replace with hardware.Identifier but retain the rest of the name.\n      // usually this is a number like Temperature 3\n      if (sensor.Name.StartsWith(\"Temperature\"))\n      {\n        name = hardware.Identifier.ToString().Replace(\"/\", \"_\").TrimStart('_') + sensor.Name.Substring(11);\n      }\n\n      // invariant culture assures the value is parsable as a float\n      var value = sensor.Value.Value.ToString(\"0.##\", CultureInfo.InvariantCulture);\n      // write the name and value to the writer\n      writer.WriteLine($\"{name}|{value}\");\n    }\n  }\n}\n"
  },
  {
    "path": "agent/lhm/beszel_lhm.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net48</TargetFramework>\n    <Platforms>x64</Platforms>\n    <RuntimeIdentifier>win-x64</RuntimeIdentifier>\n    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"LibreHardwareMonitorLib\" Version=\"0.9.5\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "agent/mdraid_linux.go",
    "content": "//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/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n)\n\n// mdraidSysfsRoot is a test hook; production value is \"/sys\".\nvar mdraidSysfsRoot = \"/sys\"\n\ntype mdraidHealth struct {\n\tlevel         string\n\tarrayState    string\n\tdegraded      uint64\n\traidDisks     uint64\n\tsyncAction    string\n\tsyncCompleted string\n\tsyncSpeed     string\n\tmismatchCnt   uint64\n\tcapacity      uint64\n}\n\n// scanMdraidDevices discovers Linux md arrays exposed in sysfs.\nfunc scanMdraidDevices() []*DeviceInfo {\n\tblockDir := filepath.Join(mdraidSysfsRoot, \"block\")\n\tentries, err := os.ReadDir(blockDir)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tdevices := make([]*DeviceInfo, 0, 2)\n\tfor _, ent := range entries {\n\t\tname := ent.Name()\n\t\tif !isMdraidBlockName(name) {\n\t\t\tcontinue\n\t\t}\n\t\tmdDir := filepath.Join(blockDir, name, \"md\")\n\t\tif !utils.FileExists(filepath.Join(mdDir, \"array_state\")) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdevPath := filepath.Join(\"/dev\", name)\n\t\tdevices = append(devices, &DeviceInfo{\n\t\t\tName:     devPath,\n\t\t\tType:     \"mdraid\",\n\t\t\tInfoName: devPath + \" [mdraid]\",\n\t\t\tProtocol: \"MD\",\n\t\t})\n\t}\n\n\treturn devices\n}\n\n// collectMdraidHealth reads mdraid health and stores it in SmartDataMap.\nfunc (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {\n\tif deviceInfo == nil || deviceInfo.Name == \"\" {\n\t\treturn false, nil\n\t}\n\n\tbase := filepath.Base(deviceInfo.Name)\n\tif !isMdraidBlockName(base) && !strings.EqualFold(deviceInfo.Type, \"mdraid\") {\n\t\treturn false, nil\n\t}\n\n\thealth, ok := readMdraidHealth(base)\n\tif !ok {\n\t\treturn false, nil\n\t}\n\n\tdeviceInfo.Type = \"mdraid\"\n\tkey := fmt.Sprintf(\"mdraid:%s\", base)\n\tstatus := mdraidSmartStatus(health)\n\n\tattrs := make([]*smart.SmartAttribute, 0, 10)\n\tif health.arrayState != \"\" {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"ArrayState\", RawString: health.arrayState})\n\t}\n\tif health.level != \"\" {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"RaidLevel\", RawString: health.level})\n\t}\n\tif health.raidDisks > 0 {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"RaidDisks\", RawValue: health.raidDisks})\n\t}\n\tif health.degraded > 0 {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"Degraded\", RawValue: health.degraded})\n\t}\n\tif health.syncAction != \"\" {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"SyncAction\", RawString: health.syncAction})\n\t}\n\tif health.syncCompleted != \"\" {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"SyncCompleted\", RawString: health.syncCompleted})\n\t}\n\tif health.syncSpeed != \"\" {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"SyncSpeed\", RawString: health.syncSpeed})\n\t}\n\tif health.mismatchCnt > 0 {\n\t\tattrs = append(attrs, &smart.SmartAttribute{Name: \"MismatchCount\", RawValue: health.mismatchCnt})\n\t}\n\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tif _, exists := sm.SmartDataMap[key]; !exists {\n\t\tsm.SmartDataMap[key] = &smart.SmartData{}\n\t}\n\n\tdata := sm.SmartDataMap[key]\n\tdata.ModelName = \"Linux MD RAID\"\n\tif health.level != \"\" {\n\t\tdata.ModelName = \"Linux MD RAID (\" + health.level + \")\"\n\t}\n\tdata.Capacity = health.capacity\n\tdata.SmartStatus = status\n\tdata.DiskName = filepath.Join(\"/dev\", base)\n\tdata.DiskType = \"mdraid\"\n\tdata.Attributes = attrs\n\n\treturn true, nil\n}\n\n// readMdraidHealth reads md array health fields from sysfs.\nfunc readMdraidHealth(blockName string) (mdraidHealth, bool) {\n\tvar out mdraidHealth\n\n\tif !isMdraidBlockName(blockName) {\n\t\treturn out, false\n\t}\n\n\tmdDir := filepath.Join(mdraidSysfsRoot, \"block\", blockName, \"md\")\n\tarrayState, okState := utils.ReadStringFileOK(filepath.Join(mdDir, \"array_state\"))\n\tif !okState {\n\t\treturn out, false\n\t}\n\n\tout.arrayState = arrayState\n\tout.level = utils.ReadStringFile(filepath.Join(mdDir, \"level\"))\n\tout.syncAction = utils.ReadStringFile(filepath.Join(mdDir, \"sync_action\"))\n\tout.syncCompleted = utils.ReadStringFile(filepath.Join(mdDir, \"sync_completed\"))\n\tout.syncSpeed = utils.ReadStringFile(filepath.Join(mdDir, \"sync_speed\"))\n\n\tif val, ok := utils.ReadUintFile(filepath.Join(mdDir, \"raid_disks\")); ok {\n\t\tout.raidDisks = val\n\t}\n\tif val, ok := utils.ReadUintFile(filepath.Join(mdDir, \"degraded\")); ok {\n\t\tout.degraded = val\n\t}\n\tif val, ok := utils.ReadUintFile(filepath.Join(mdDir, \"mismatch_cnt\")); ok {\n\t\tout.mismatchCnt = val\n\t}\n\n\tif capBytes, ok := readMdraidBlockCapacityBytes(blockName, mdraidSysfsRoot); ok {\n\t\tout.capacity = capBytes\n\t}\n\n\treturn out, true\n}\n\n// mdraidSmartStatus maps md state/sync signals to a SMART-like status.\nfunc mdraidSmartStatus(health mdraidHealth) string {\n\tstate := strings.ToLower(strings.TrimSpace(health.arrayState))\n\tswitch state {\n\tcase \"inactive\", \"faulty\", \"broken\", \"stopped\":\n\t\treturn \"FAILED\"\n\t}\n\t// During rebuild/recovery, arrays are often temporarily degraded; report as\n\t// warning instead of hard failure while synchronization is in progress.\n\tsyncAction := strings.ToLower(strings.TrimSpace(health.syncAction))\n\tswitch syncAction {\n\tcase \"resync\", \"recover\", \"reshape\":\n\t\treturn \"WARNING\"\n\t}\n\tif health.degraded > 0 {\n\t\treturn \"FAILED\"\n\t}\n\tswitch syncAction {\n\tcase \"check\", \"repair\":\n\t\treturn \"WARNING\"\n\t}\n\tswitch state {\n\tcase \"clean\", \"active\", \"active-idle\", \"write-pending\", \"read-auto\", \"readonly\":\n\t\treturn \"PASSED\"\n\t}\n\treturn \"UNKNOWN\"\n}\n\n// isMdraidBlockName matches /dev/mdN-style block device names.\nfunc isMdraidBlockName(name string) bool {\n\tif !strings.HasPrefix(name, \"md\") {\n\t\treturn false\n\t}\n\tsuffix := strings.TrimPrefix(name, \"md\")\n\tif suffix == \"\" {\n\t\treturn false\n\t}\n\tfor _, c := range suffix {\n\t\tif c < '0' || c > '9' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// readMdraidBlockCapacityBytes converts block size metadata into bytes.\nfunc readMdraidBlockCapacityBytes(blockName, root string) (uint64, bool) {\n\tsizePath := filepath.Join(root, \"block\", blockName, \"size\")\n\tlbsPath := filepath.Join(root, \"block\", blockName, \"queue\", \"logical_block_size\")\n\n\tsizeStr, ok := utils.ReadStringFileOK(sizePath)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tsectors, err := strconv.ParseUint(sizeStr, 10, 64)\n\tif err != nil || sectors == 0 {\n\t\treturn 0, false\n\t}\n\n\tlogicalBlockSize := uint64(512)\n\tif lbsStr, ok := utils.ReadStringFileOK(lbsPath); ok {\n\t\tif parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {\n\t\t\tlogicalBlockSize = parsed\n\t\t}\n\t}\n\n\treturn sectors * logicalBlockSize, true\n}\n"
  },
  {
    "path": "agent/mdraid_linux_test.go",
    "content": "//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/entities/smart\"\n)\n\nfunc TestMdraidMockSysfsScanAndCollect(t *testing.T) {\n\ttmp := t.TempDir()\n\tprev := mdraidSysfsRoot\n\tmdraidSysfsRoot = tmp\n\tt.Cleanup(func() { mdraidSysfsRoot = prev })\n\n\tmdDir := filepath.Join(tmp, \"block\", \"md0\", \"md\")\n\tqueueDir := filepath.Join(tmp, \"block\", \"md0\", \"queue\")\n\tif err := os.MkdirAll(mdDir, 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.MkdirAll(queueDir, 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twrite := func(path, content string) {\n\t\tt.Helper()\n\t\tif err := os.WriteFile(path, []byte(content), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\n\twrite(filepath.Join(mdDir, \"array_state\"), \"active\\n\")\n\twrite(filepath.Join(mdDir, \"level\"), \"raid1\\n\")\n\twrite(filepath.Join(mdDir, \"raid_disks\"), \"2\\n\")\n\twrite(filepath.Join(mdDir, \"degraded\"), \"0\\n\")\n\twrite(filepath.Join(mdDir, \"sync_action\"), \"resync\\n\")\n\twrite(filepath.Join(mdDir, \"sync_completed\"), \"10%\\n\")\n\twrite(filepath.Join(mdDir, \"sync_speed\"), \"100M\\n\")\n\twrite(filepath.Join(mdDir, \"mismatch_cnt\"), \"0\\n\")\n\twrite(filepath.Join(queueDir, \"logical_block_size\"), \"512\\n\")\n\twrite(filepath.Join(tmp, \"block\", \"md0\", \"size\"), \"2048\\n\")\n\n\tdevs := scanMdraidDevices()\n\tif len(devs) != 1 {\n\t\tt.Fatalf(\"scanMdraidDevices() = %d devices, want 1\", len(devs))\n\t}\n\tif devs[0].Name != \"/dev/md0\" || devs[0].Type != \"mdraid\" {\n\t\tt.Fatalf(\"scanMdraidDevices()[0] = %+v, want Name=/dev/md0 Type=mdraid\", devs[0])\n\t}\n\n\tsm := &SmartManager{SmartDataMap: map[string]*smart.SmartData{}}\n\tok, err := sm.collectMdraidHealth(devs[0])\n\tif err != nil || !ok {\n\t\tt.Fatalf(\"collectMdraidHealth() = (ok=%v, err=%v), want (true,nil)\", ok, err)\n\t}\n\tif len(sm.SmartDataMap) != 1 {\n\t\tt.Fatalf(\"SmartDataMap len=%d, want 1\", len(sm.SmartDataMap))\n\t}\n\tvar got *smart.SmartData\n\tfor _, v := range sm.SmartDataMap {\n\t\tgot = v\n\t\tbreak\n\t}\n\tif got == nil {\n\t\tt.Fatalf(\"SmartDataMap value nil\")\n\t}\n\tif got.DiskType != \"mdraid\" || got.DiskName != \"/dev/md0\" {\n\t\tt.Fatalf(\"disk fields = (type=%q name=%q), want (mdraid,/dev/md0)\", got.DiskType, got.DiskName)\n\t}\n\tif got.SmartStatus != \"WARNING\" {\n\t\tt.Fatalf(\"SmartStatus=%q, want WARNING\", got.SmartStatus)\n\t}\n\tif got.ModelName == \"\" || got.Capacity == 0 {\n\t\tt.Fatalf(\"identity fields = (model=%q cap=%d), want non-empty model and cap>0\", got.ModelName, got.Capacity)\n\t}\n\tif len(got.Attributes) < 5 {\n\t\tt.Fatalf(\"attributes len=%d, want >= 5\", len(got.Attributes))\n\t}\n}\n\nfunc TestMdraidSmartStatus(t *testing.T) {\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"inactive\"}); got != \"FAILED\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(inactive) = %q, want FAILED\", got)\n\t}\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"active\", degraded: 1, syncAction: \"recover\"}); got != \"WARNING\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(degraded+recover) = %q, want WARNING\", got)\n\t}\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"active\", degraded: 1}); got != \"FAILED\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(degraded) = %q, want FAILED\", got)\n\t}\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"active\", syncAction: \"recover\"}); got != \"WARNING\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(recover) = %q, want WARNING\", got)\n\t}\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"clean\"}); got != \"PASSED\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(clean) = %q, want PASSED\", got)\n\t}\n\tif got := mdraidSmartStatus(mdraidHealth{arrayState: \"unknown\"}); got != \"UNKNOWN\" {\n\t\tt.Fatalf(\"mdraidSmartStatus(unknown) = %q, want UNKNOWN\", got)\n\t}\n}\n"
  },
  {
    "path": "agent/mdraid_stub.go",
    "content": "//go:build !linux\n\npackage agent\n\nfunc scanMdraidDevices() []*DeviceInfo {\n\treturn nil\n}\n\nfunc (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {\n\treturn false, nil\n}\n"
  },
  {
    "path": "agent/network.go",
    "content": "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\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\tpsutilNet \"github.com/shirou/gopsutil/v4/net\"\n)\n\n// NicConfig controls inclusion/exclusion of network interfaces via the NICS env var\n//\n// Behavior mirrors SensorConfig's matching logic:\n// - Leading '-' means blacklist mode; otherwise whitelist mode\n// - Supports '*' wildcards using path.Match\n// - In whitelist mode with an empty list, no NICs are selected\n// - In blacklist mode with an empty list, all NICs are selected\ntype NicConfig struct {\n\tnics         map[string]struct{}\n\tisBlacklist  bool\n\thasWildcards bool\n}\n\nfunc newNicConfig(nicsEnvVal string) *NicConfig {\n\tcfg := &NicConfig{\n\t\tnics: make(map[string]struct{}),\n\t}\n\tif strings.HasPrefix(nicsEnvVal, \"-\") {\n\t\tcfg.isBlacklist = true\n\t\tnicsEnvVal = nicsEnvVal[1:]\n\t}\n\tfor nic := range strings.SplitSeq(nicsEnvVal, \",\") {\n\t\tnic = strings.TrimSpace(nic)\n\t\tif nic != \"\" {\n\t\t\tcfg.nics[nic] = struct{}{}\n\t\t\tif strings.Contains(nic, \"*\") {\n\t\t\t\tcfg.hasWildcards = true\n\t\t\t}\n\t\t}\n\t}\n\treturn cfg\n}\n\n// isValidNic determines if a NIC should be included based on NicConfig rules\nfunc isValidNic(nicName string, cfg *NicConfig) bool {\n\t// Empty list behavior differs by mode: blacklist: allow all; whitelist: allow none\n\tif len(cfg.nics) == 0 {\n\t\treturn cfg.isBlacklist\n\t}\n\n\t// Exact match: return true if whitelist, false if blacklist\n\tif _, exactMatch := cfg.nics[nicName]; exactMatch {\n\t\treturn !cfg.isBlacklist\n\t}\n\n\t// If no wildcards, return true if blacklist, false if whitelist\n\tif !cfg.hasWildcards {\n\t\treturn cfg.isBlacklist\n\t}\n\n\t// Check for wildcard patterns\n\tfor pattern := range cfg.nics {\n\t\tif !strings.Contains(pattern, \"*\") {\n\t\t\tcontinue\n\t\t}\n\t\tif match, _ := path.Match(pattern, nicName); match {\n\t\t\treturn !cfg.isBlacklist\n\t\t}\n\t}\n\n\treturn cfg.isBlacklist\n}\n\nfunc (a *Agent) updateNetworkStats(cacheTimeMs uint16, systemStats *system.Stats) {\n\t// network stats\n\ta.ensureNetInterfacesInitialized()\n\n\ta.ensureNetworkInterfacesMap(systemStats)\n\n\tif netIO, err := psutilNet.IOCounters(true); err == nil {\n\t\tnis, msElapsed := a.loadAndTickNetBaseline(cacheTimeMs)\n\t\ttotalBytesSent, totalBytesRecv := a.sumAndTrackPerNicDeltas(cacheTimeMs, msElapsed, netIO, systemStats)\n\t\tbytesSentPerSecond, bytesRecvPerSecond := a.computeBytesPerSecond(msElapsed, totalBytesSent, totalBytesRecv, nis)\n\t\ta.applyNetworkTotals(cacheTimeMs, netIO, systemStats, nis, totalBytesSent, totalBytesRecv, bytesSentPerSecond, bytesRecvPerSecond)\n\t}\n}\n\nfunc (a *Agent) initializeNetIoStats() {\n\t// reset valid network interfaces\n\ta.netInterfaces = make(map[string]struct{}, 0)\n\n\t// parse NICS env var for whitelist / blacklist\n\tnicsEnvVal, nicsEnvExists := utils.GetEnv(\"NICS\")\n\tvar nicCfg *NicConfig\n\tif nicsEnvExists {\n\t\tnicCfg = newNicConfig(nicsEnvVal)\n\t}\n\n\t// get current network I/O stats and record valid interfaces\n\tif netIO, err := psutilNet.IOCounters(true); err == nil {\n\t\tfor _, v := range netIO {\n\t\t\tif skipNetworkInterface(v, nicCfg) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tslog.Info(\"Detected network interface\", \"name\", v.Name, \"sent\", v.BytesSent, \"recv\", v.BytesRecv)\n\t\t\t// store as a valid network interface\n\t\t\ta.netInterfaces[v.Name] = struct{}{}\n\t\t}\n\t}\n\n\t// Reset per-cache-time trackers and baselines so they will reinitialize on next use\n\ta.netInterfaceDeltaTrackers = make(map[uint16]*deltatracker.DeltaTracker[string, uint64])\n\ta.netIoStats = make(map[uint16]system.NetIoStats)\n}\n\n// ensureNetInterfacesInitialized re-initializes NICs if none are currently tracked\nfunc (a *Agent) ensureNetInterfacesInitialized() {\n\tif len(a.netInterfaces) == 0 {\n\t\t// if no network interfaces, initialize again\n\t\t// this is a fix if agent started before network is online (#466)\n\t\t// maybe refactor this in the future to not cache interface names at all so we\n\t\t// don't miss an interface that's been added after agent started in any circumstance\n\t\ta.initializeNetIoStats()\n\t}\n}\n\n// ensureNetworkInterfacesMap ensures systemStats.NetworkInterfaces map exists\nfunc (a *Agent) ensureNetworkInterfacesMap(systemStats *system.Stats) {\n\tif systemStats.NetworkInterfaces == nil {\n\t\tsystemStats.NetworkInterfaces = make(map[string][4]uint64, 0)\n\t}\n}\n\n// loadAndTickNetBaseline returns the NetIoStats baseline and milliseconds elapsed, updating time\nfunc (a *Agent) loadAndTickNetBaseline(cacheTimeMs uint16) (netIoStat system.NetIoStats, msElapsed uint64) {\n\tnetIoStat = a.netIoStats[cacheTimeMs]\n\tif netIoStat.Time.IsZero() {\n\t\tnetIoStat.Time = time.Now()\n\t\tmsElapsed = 0\n\t} else {\n\t\tmsElapsed = uint64(time.Since(netIoStat.Time).Milliseconds())\n\t\tnetIoStat.Time = time.Now()\n\t}\n\treturn netIoStat, msElapsed\n}\n\n// sumAndTrackPerNicDeltas accumulates totals and records per-NIC up/down deltas into systemStats\nfunc (a *Agent) sumAndTrackPerNicDeltas(cacheTimeMs uint16, msElapsed uint64, netIO []psutilNet.IOCountersStat, systemStats *system.Stats) (totalBytesSent, totalBytesRecv uint64) {\n\ttracker := a.netInterfaceDeltaTrackers[cacheTimeMs]\n\tif tracker == nil {\n\t\ttracker = deltatracker.NewDeltaTracker[string, uint64]()\n\t\ta.netInterfaceDeltaTrackers[cacheTimeMs] = tracker\n\t}\n\ttracker.Cycle()\n\n\tfor _, v := range netIO {\n\t\tif _, exists := a.netInterfaces[v.Name]; !exists {\n\t\t\tcontinue\n\t\t}\n\t\ttotalBytesSent += v.BytesSent\n\t\ttotalBytesRecv += v.BytesRecv\n\n\t\tvar upDelta, downDelta uint64\n\t\tupKey, downKey := fmt.Sprintf(\"%sup\", v.Name), fmt.Sprintf(\"%sdown\", v.Name)\n\t\ttracker.Set(upKey, v.BytesSent)\n\t\ttracker.Set(downKey, v.BytesRecv)\n\t\tif msElapsed > 0 {\n\t\t\tif prevVal, ok := tracker.Previous(upKey); ok {\n\t\t\t\tvar deltaBytes uint64\n\t\t\t\tif v.BytesSent >= prevVal {\n\t\t\t\t\tdeltaBytes = v.BytesSent - prevVal\n\t\t\t\t} else {\n\t\t\t\t\tdeltaBytes = v.BytesSent\n\t\t\t\t}\n\t\t\t\tupDelta = deltaBytes * 1000 / msElapsed\n\t\t\t}\n\t\t\tif prevVal, ok := tracker.Previous(downKey); ok {\n\t\t\t\tvar deltaBytes uint64\n\t\t\t\tif v.BytesRecv >= prevVal {\n\t\t\t\t\tdeltaBytes = v.BytesRecv - prevVal\n\t\t\t\t} else {\n\t\t\t\t\tdeltaBytes = v.BytesRecv\n\t\t\t\t}\n\t\t\t\tdownDelta = deltaBytes * 1000 / msElapsed\n\t\t\t}\n\t\t}\n\t\tsystemStats.NetworkInterfaces[v.Name] = [4]uint64{upDelta, downDelta, v.BytesSent, v.BytesRecv}\n\t}\n\n\treturn totalBytesSent, totalBytesRecv\n}\n\n// computeBytesPerSecond calculates per-second totals from elapsed time and totals\nfunc (a *Agent) computeBytesPerSecond(msElapsed, totalBytesSent, totalBytesRecv uint64, nis system.NetIoStats) (bytesSentPerSecond, bytesRecvPerSecond uint64) {\n\tif msElapsed > 0 {\n\t\tbytesSentPerSecond = (totalBytesSent - nis.BytesSent) * 1000 / msElapsed\n\t\tbytesRecvPerSecond = (totalBytesRecv - nis.BytesRecv) * 1000 / msElapsed\n\t}\n\treturn bytesSentPerSecond, bytesRecvPerSecond\n}\n\n// applyNetworkTotals validates and writes computed network stats, or resets on anomaly\nfunc (a *Agent) applyNetworkTotals(\n\tcacheTimeMs uint16,\n\tnetIO []psutilNet.IOCountersStat,\n\tsystemStats *system.Stats,\n\tnis system.NetIoStats,\n\ttotalBytesSent, totalBytesRecv uint64,\n\tbytesSentPerSecond, bytesRecvPerSecond uint64,\n) {\n\tif bytesSentPerSecond > 10_000_000_000 || bytesRecvPerSecond > 10_000_000_000 {\n\t\tslog.Warn(\"Invalid net stats. Resetting.\", \"sent\", bytesSentPerSecond, \"recv\", bytesRecvPerSecond)\n\t\tfor _, v := range netIO {\n\t\t\tif _, exists := a.netInterfaces[v.Name]; !exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tslog.Info(v.Name, \"recv\", v.BytesRecv, \"sent\", v.BytesSent)\n\t\t}\n\t\ta.initializeNetIoStats()\n\t\tdelete(a.netIoStats, cacheTimeMs)\n\t\tdelete(a.netInterfaceDeltaTrackers, cacheTimeMs)\n\t\tsystemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0\n\t\treturn\n\t}\n\n\tsystemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond\n\tnis.BytesSent = totalBytesSent\n\tnis.BytesRecv = totalBytesRecv\n\ta.netIoStats[cacheTimeMs] = nis\n}\n\n// skipNetworkInterface returns true if the network interface should be ignored.\nfunc skipNetworkInterface(v psutilNet.IOCountersStat, nicCfg *NicConfig) bool {\n\tif nicCfg != nil {\n\t\tif !isValidNic(v.Name, nicCfg) {\n\t\t\treturn true\n\t\t}\n\t\t// In whitelist mode, we honor explicit inclusion without auto-filtering.\n\t\tif !nicCfg.isBlacklist {\n\t\t\treturn false\n\t\t}\n\t\t// In blacklist mode, still apply the auto-filter below.\n\t}\n\n\tswitch {\n\tcase strings.HasPrefix(v.Name, \"lo\"),\n\t\tstrings.HasPrefix(v.Name, \"docker\"),\n\t\tstrings.HasPrefix(v.Name, \"br-\"),\n\t\tstrings.HasPrefix(v.Name, \"veth\"),\n\t\tstrings.HasPrefix(v.Name, \"bond\"),\n\t\tstrings.HasPrefix(v.Name, \"cali\"),\n\t\tv.BytesRecv == 0,\n\t\tv.BytesSent == 0:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "agent/network_test.go",
    "content": "//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.com/henrygd/beszel/internal/entities/system\"\n\tpsutilNet \"github.com/shirou/gopsutil/v4/net\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsValidNic(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tnicName       string\n\t\tconfig        *NicConfig\n\t\texpectedValid bool\n\t}{\n\t\t{\n\t\t\tname:    \"Whitelist - NIC in list\",\n\t\t\tnicName: \"eth0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{\"eth0\": {}},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Whitelist - NIC not in list\",\n\t\t\tnicName: \"wlan0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{\"eth0\": {}},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Blacklist - NIC in list\",\n\t\t\tnicName: \"eth0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{\"eth0\": {}},\n\t\t\t\tisBlacklist: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Blacklist - NIC not in list\",\n\t\t\tnicName: \"wlan0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{\"eth0\": {}},\n\t\t\t\tisBlacklist: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Whitelist with wildcard - matching pattern\",\n\t\t\tnicName: \"eth1\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Whitelist with wildcard - non-matching pattern\",\n\t\t\tnicName: \"wlan0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Blacklist with wildcard - matching pattern\",\n\t\t\tnicName: \"eth1\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Blacklist with wildcard - non-matching pattern\",\n\t\t\tnicName: \"wlan0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Empty whitelist config - no NICs allowed\",\n\t\t\tnicName: \"eth0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"Empty blacklist config - all NICs allowed\",\n\t\t\tnicName: \"eth0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{},\n\t\t\t\tisBlacklist: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Multiple patterns - exact match\",\n\t\t\tnicName: \"eth0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:        map[string]struct{}{\"eth0\": {}, \"wlan*\": {}},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Multiple patterns - wildcard match\",\n\t\t\tnicName: \"wlan1\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"wlan*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"Multiple patterns - no match\",\n\t\t\tnicName: \"bond0\",\n\t\t\tconfig: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"wlan*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isValidNic(tt.nicName, tt.config)\n\t\t\tassert.Equal(t, tt.expectedValid, result)\n\t\t})\n\t}\n}\n\nfunc TestNewNicConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tnicsEnvVal  string\n\t\texpectedCfg *NicConfig\n\t}{\n\t\t{\n\t\t\tname:       \"Empty string\",\n\t\t\tnicsEnvVal: \"\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Single NIC whitelist\",\n\t\t\tnicsEnvVal: \"eth0\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Multiple NICs whitelist\",\n\t\t\tnicsEnvVal: \"eth0,wlan0\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"wlan0\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist mode\",\n\t\t\tnicsEnvVal: \"-eth0,wlan0\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"wlan0\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"With wildcards\",\n\t\t\tnicsEnvVal: \"eth*,wlan0\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}, \"wlan0\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist with wildcards\",\n\t\t\tnicsEnvVal: \"-eth*,wlan0\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}, \"wlan0\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"With whitespace\",\n\t\t\tnicsEnvVal: \"eth0, wlan0 , eth1\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"wlan0\": {}, \"eth1\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Only wildcards\",\n\t\t\tnicsEnvVal: \"eth*,wlan*\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth*\": {}, \"wlan*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Leading dash only\",\n\t\t\tnicsEnvVal: \"-\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"Mixed exact and wildcard\",\n\t\t\tnicsEnvVal: \"eth0,br-*\",\n\t\t\texpectedCfg: &NicConfig{\n\t\t\t\tnics:         map[string]struct{}{\"eth0\": {}, \"br-*\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcfg := newNicConfig(tt.nicsEnvVal)\n\t\t\trequire.NotNil(t, cfg)\n\t\t\tassert.Equal(t, tt.expectedCfg.isBlacklist, cfg.isBlacklist)\n\t\t\tassert.Equal(t, tt.expectedCfg.hasWildcards, cfg.hasWildcards)\n\t\t\tassert.Equal(t, tt.expectedCfg.nics, cfg.nics)\n\t\t})\n\t}\n}\nfunc TestSkipNetworkInterface(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tnic        psutilNet.IOCountersStat\n\t\tnicCfg     *NicConfig\n\t\texpectSkip bool\n\t}{\n\t\t{\"loopback lo\", psutilNet.IOCountersStat{Name: \"lo\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"loopback lo0\", psutilNet.IOCountersStat{Name: \"lo0\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"docker prefix\", psutilNet.IOCountersStat{Name: \"docker0\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"br- prefix\", psutilNet.IOCountersStat{Name: \"br-lan\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"veth prefix\", psutilNet.IOCountersStat{Name: \"veth0abc\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"bond prefix\", psutilNet.IOCountersStat{Name: \"bond0\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"cali prefix\", psutilNet.IOCountersStat{Name: \"cali1234\", BytesSent: 100, BytesRecv: 100}, nil, true},\n\t\t{\"zero BytesRecv\", psutilNet.IOCountersStat{Name: \"eth0\", BytesSent: 100, BytesRecv: 0}, nil, true},\n\t\t{\"zero BytesSent\", psutilNet.IOCountersStat{Name: \"eth0\", BytesSent: 0, BytesRecv: 100}, nil, true},\n\t\t{\"both zero\", psutilNet.IOCountersStat{Name: \"eth0\", BytesSent: 0, BytesRecv: 0}, nil, true},\n\t\t{\"normal eth0\", psutilNet.IOCountersStat{Name: \"eth0\", BytesSent: 100, BytesRecv: 200}, nil, false},\n\t\t{\"normal wlan0\", psutilNet.IOCountersStat{Name: \"wlan0\", BytesSent: 1, BytesRecv: 1}, nil, false},\n\t\t{\"whitelist overrides skip (docker)\", psutilNet.IOCountersStat{Name: \"docker0\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"docker0\"), false},\n\t\t{\"whitelist overrides skip (lo)\", psutilNet.IOCountersStat{Name: \"lo\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"lo\"), false},\n\t\t{\"whitelist exclusion\", psutilNet.IOCountersStat{Name: \"eth1\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"eth0\"), true},\n\t\t{\"blacklist skip lo\", psutilNet.IOCountersStat{Name: \"lo\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"-eth0\"), true},\n\t\t{\"blacklist explicit eth0\", psutilNet.IOCountersStat{Name: \"eth0\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"-eth0\"), true},\n\t\t{\"blacklist allow eth1\", psutilNet.IOCountersStat{Name: \"eth1\", BytesSent: 100, BytesRecv: 100}, newNicConfig(\"-eth0\"), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expectSkip, skipNetworkInterface(tt.nic, tt.nicCfg))\n\t\t})\n\t}\n}\n\nfunc TestEnsureNetworkInterfacesMap(t *testing.T) {\n\tvar a Agent\n\tvar stats system.Stats\n\n\t// Initially nil\n\tassert.Nil(t, stats.NetworkInterfaces)\n\t// Ensure map is created\n\ta.ensureNetworkInterfacesMap(&stats)\n\tassert.NotNil(t, stats.NetworkInterfaces)\n\t// Idempotent\n\ta.ensureNetworkInterfacesMap(&stats)\n\tassert.NotNil(t, stats.NetworkInterfaces)\n}\n\nfunc TestLoadAndTickNetBaseline(t *testing.T) {\n\ta := &Agent{netIoStats: make(map[uint16]system.NetIoStats)}\n\n\t// First call initializes time and returns 0 elapsed\n\tni, elapsed := a.loadAndTickNetBaseline(100)\n\tassert.Equal(t, uint64(0), elapsed)\n\tassert.False(t, ni.Time.IsZero())\n\n\t// Store back what loadAndTick returns to mimic updateNetworkStats behavior\n\ta.netIoStats[100] = ni\n\n\ttime.Sleep(2 * time.Millisecond)\n\n\t// Next call should produce >= 0 elapsed and update time\n\tni2, elapsed2 := a.loadAndTickNetBaseline(100)\n\tassert.True(t, elapsed2 > 0)\n\tassert.False(t, ni2.Time.IsZero())\n}\n\nfunc TestComputeBytesPerSecond(t *testing.T) {\n\ta := &Agent{}\n\n\t// No elapsed -> zero rate\n\tbytesUp, bytesDown := a.computeBytesPerSecond(0, 2000, 3000, system.NetIoStats{BytesSent: 1000, BytesRecv: 1000})\n\tassert.Equal(t, uint64(0), bytesUp)\n\tassert.Equal(t, uint64(0), bytesDown)\n\n\t// With elapsed -> per-second calculation\n\tbytesUp, bytesDown = a.computeBytesPerSecond(500, 6000, 11000, system.NetIoStats{BytesSent: 1000, BytesRecv: 1000})\n\t// (6000-1000)*1000/500 = 10000; (11000-1000)*1000/500 = 20000\n\tassert.Equal(t, uint64(10000), bytesUp)\n\tassert.Equal(t, uint64(20000), bytesDown)\n}\n\nfunc TestSumAndTrackPerNicDeltas(t *testing.T) {\n\ta := &Agent{\n\t\tnetInterfaces:             map[string]struct{}{\"eth0\": {}, \"wlan0\": {}},\n\t\tnetInterfaceDeltaTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\t// Two samples for same cache interval to verify delta behavior\n\tcache := uint16(42)\n\tnet1 := []psutilNet.IOCountersStat{{Name: \"eth0\", BytesSent: 1000, BytesRecv: 2000}}\n\tstats1 := &system.Stats{}\n\ta.ensureNetworkInterfacesMap(stats1)\n\ttx1, rx1 := a.sumAndTrackPerNicDeltas(cache, 0, net1, stats1)\n\tassert.Equal(t, uint64(1000), tx1)\n\tassert.Equal(t, uint64(2000), rx1)\n\n\t// Second cycle with elapsed, larger counters -> deltas computed inside\n\tnet2 := []psutilNet.IOCountersStat{{Name: \"eth0\", BytesSent: 4000, BytesRecv: 9000}}\n\tstats := &system.Stats{}\n\ta.ensureNetworkInterfacesMap(stats)\n\ttx2, rx2 := a.sumAndTrackPerNicDeltas(cache, 1000, net2, stats)\n\tassert.Equal(t, uint64(4000), tx2)\n\tassert.Equal(t, uint64(9000), rx2)\n\t// Up/Down deltas per second should be (4000-1000)/1s = 3000 and (9000-2000)/1s = 7000\n\tni, ok := stats.NetworkInterfaces[\"eth0\"]\n\tassert.True(t, ok)\n\tassert.Equal(t, uint64(3000), ni[0])\n\tassert.Equal(t, uint64(7000), ni[1])\n}\n\nfunc TestSumAndTrackPerNicDeltasHandlesCounterReset(t *testing.T) {\n\ta := &Agent{\n\t\tnetInterfaces:             map[string]struct{}{\"eth0\": {}},\n\t\tnetInterfaceDeltaTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t}\n\n\tcache := uint16(77)\n\n\t// First interval establishes baseline values\n\tinitial := []psutilNet.IOCountersStat{{Name: \"eth0\", BytesSent: 4_000, BytesRecv: 6_000}}\n\tstatsInitial := &system.Stats{}\n\ta.ensureNetworkInterfacesMap(statsInitial)\n\t_, _ = a.sumAndTrackPerNicDeltas(cache, 0, initial, statsInitial)\n\n\t// Second interval increments counters normally so previous snapshot gets populated\n\tincrement := []psutilNet.IOCountersStat{{Name: \"eth0\", BytesSent: 9_000, BytesRecv: 11_000}}\n\tstatsIncrement := &system.Stats{}\n\ta.ensureNetworkInterfacesMap(statsIncrement)\n\t_, _ = a.sumAndTrackPerNicDeltas(cache, 1_000, increment, statsIncrement)\n\n\tniIncrement, ok := statsIncrement.NetworkInterfaces[\"eth0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, uint64(5_000), niIncrement[0])\n\tassert.Equal(t, uint64(5_000), niIncrement[1])\n\n\t// Third interval simulates counter reset (values drop below previous totals)\n\treset := []psutilNet.IOCountersStat{{Name: \"eth0\", BytesSent: 1_200, BytesRecv: 1_500}}\n\tstatsReset := &system.Stats{}\n\ta.ensureNetworkInterfacesMap(statsReset)\n\t_, _ = a.sumAndTrackPerNicDeltas(cache, 1_000, reset, statsReset)\n\n\tniReset, ok := statsReset.NetworkInterfaces[\"eth0\"]\n\trequire.True(t, ok)\n\tassert.Equal(t, uint64(1_200), niReset[0], \"upload delta should match new counter value after reset\")\n\tassert.Equal(t, uint64(1_500), niReset[1], \"download delta should match new counter value after reset\")\n}\n\nfunc TestApplyNetworkTotals(t *testing.T) {\n\ttests := []struct {\n\t\tname                  string\n\t\tbytesSentPerSecond    uint64\n\t\tbytesRecvPerSecond    uint64\n\t\ttotalBytesSent        uint64\n\t\ttotalBytesRecv        uint64\n\t\texpectReset           bool\n\t\texpectedBandwidthSent uint64\n\t\texpectedBandwidthRecv uint64\n\t}{\n\t\t{\n\t\t\tname:                  \"Valid network stats - normal values\",\n\t\t\tbytesSentPerSecond:    1000000, // 1 MB/s\n\t\t\tbytesRecvPerSecond:    2000000, // 2 MB/s\n\t\t\ttotalBytesSent:        10000000,\n\t\t\ttotalBytesRecv:        20000000,\n\t\t\texpectReset:           false,\n\t\t\texpectedBandwidthSent: 1000000,\n\t\t\texpectedBandwidthRecv: 2000000,\n\t\t},\n\t\t{\n\t\t\tname:               \"Invalid network stats - sent exceeds threshold\",\n\t\t\tbytesSentPerSecond: 11000000000, // ~10.5 GB/s > 10 GB/s threshold\n\t\t\tbytesRecvPerSecond: 1000000,     // 1 MB/s\n\t\t\ttotalBytesSent:     10000000,\n\t\t\ttotalBytesRecv:     20000000,\n\t\t\texpectReset:        true,\n\t\t},\n\t\t{\n\t\t\tname:               \"Invalid network stats - recv exceeds threshold\",\n\t\t\tbytesSentPerSecond: 1000000,     // 1 MB/s\n\t\t\tbytesRecvPerSecond: 11000000000, // ~10.5 GB/s > 10 GB/s threshold\n\t\t\ttotalBytesSent:     10000000,\n\t\t\ttotalBytesRecv:     20000000,\n\t\t\texpectReset:        true,\n\t\t},\n\t\t{\n\t\t\tname:               \"Invalid network stats - both exceed threshold\",\n\t\t\tbytesSentPerSecond: 12000000000, // ~11.4 GB/s\n\t\t\tbytesRecvPerSecond: 13000000000, // ~12.4 GB/s\n\t\t\ttotalBytesSent:     10000000,\n\t\t\ttotalBytesRecv:     20000000,\n\t\t\texpectReset:        true,\n\t\t},\n\t\t{\n\t\t\tname:                  \"Zero values\",\n\t\t\tbytesSentPerSecond:    0,\n\t\t\tbytesRecvPerSecond:    0,\n\t\t\ttotalBytesSent:        0,\n\t\t\ttotalBytesRecv:        0,\n\t\t\texpectReset:           false,\n\t\t\texpectedBandwidthSent: 0,\n\t\t\texpectedBandwidthRecv: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Setup agent with initialized maps\n\t\t\ta := &Agent{\n\t\t\t\tnetInterfaces:             make(map[string]struct{}),\n\t\t\t\tnetIoStats:                make(map[uint16]system.NetIoStats),\n\t\t\t\tnetInterfaceDeltaTrackers: make(map[uint16]*deltatracker.DeltaTracker[string, uint64]),\n\t\t\t}\n\n\t\t\tcacheTimeMs := uint16(100)\n\t\t\tnetIO := []psutilNet.IOCountersStat{\n\t\t\t\t{Name: \"eth0\", BytesSent: 1000, BytesRecv: 2000},\n\t\t\t}\n\t\t\tsystemStats := &system.Stats{}\n\t\t\tnis := system.NetIoStats{}\n\n\t\t\ta.applyNetworkTotals(\n\t\t\t\tcacheTimeMs,\n\t\t\t\tnetIO,\n\t\t\t\tsystemStats,\n\t\t\t\tnis,\n\t\t\t\ttt.totalBytesSent,\n\t\t\t\ttt.totalBytesRecv,\n\t\t\t\ttt.bytesSentPerSecond,\n\t\t\t\ttt.bytesRecvPerSecond,\n\t\t\t)\n\n\t\t\tif tt.expectReset {\n\t\t\t\t// Should have reset network tracking state - maps cleared and stats zeroed\n\t\t\t\tassert.NotContains(t, a.netIoStats, cacheTimeMs, \"cache entry should be cleared after reset\")\n\t\t\t\tassert.NotContains(t, a.netInterfaceDeltaTrackers, cacheTimeMs, \"tracker should be cleared on reset\")\n\t\t\t\tassert.Zero(t, systemStats.Bandwidth[0])\n\t\t\t\tassert.Zero(t, systemStats.Bandwidth[1])\n\t\t\t} else {\n\t\t\t\t// Should have applied stats\n\t\t\t\tassert.Equal(t, tt.expectedBandwidthSent, systemStats.Bandwidth[0])\n\t\t\t\tassert.Equal(t, tt.expectedBandwidthRecv, systemStats.Bandwidth[1])\n\n\t\t\t\t// Should have updated NetIoStats\n\t\t\t\tupdatedNis := a.netIoStats[cacheTimeMs]\n\t\t\t\tassert.Equal(t, tt.totalBytesSent, updatedNis.BytesSent)\n\t\t\t\tassert.Equal(t, tt.totalBytesRecv, updatedNis.BytesRecv)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "agent/response.go",
    "content": "package agent\n\nimport (\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\n// newAgentResponse creates an AgentResponse using legacy typed fields.\n// This maintains backward compatibility with <= 0.17 hubs that expect specific fields.\nfunc newAgentResponse(data any, requestID *uint32) common.AgentResponse {\n\tresponse := common.AgentResponse{Id: requestID}\n\tswitch v := data.(type) {\n\tcase *system.CombinedData:\n\t\tresponse.SystemData = v\n\tcase *common.FingerprintResponse:\n\t\tresponse.Fingerprint = v\n\tcase string:\n\t\tresponse.String = &v\n\tcase map[string]smart.SmartData:\n\t\tresponse.SmartData = v\n\tcase systemd.ServiceDetails:\n\t\tresponse.ServiceInfo = v\n\tdefault:\n\t\t// For unknown types, use the generic Data field\n\t\tresponse.Data, _ = cbor.Marshal(data)\n\t}\n\treturn response\n}\n"
  },
  {
    "path": "agent/sensors.go",
    "content": "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.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/shirou/gopsutil/v4/common\"\n\t\"github.com/shirou/gopsutil/v4/sensors\"\n)\n\ntype SensorConfig struct {\n\tcontext        context.Context\n\tsensors        map[string]struct{}\n\tprimarySensor  string\n\tisBlacklist    bool\n\thasWildcards   bool\n\tskipCollection bool\n}\n\nfunc (a *Agent) newSensorConfig() *SensorConfig {\n\tprimarySensor, _ := utils.GetEnv(\"PRIMARY_SENSOR\")\n\tsysSensors, _ := utils.GetEnv(\"SYS_SENSORS\")\n\tsensorsEnvVal, sensorsSet := utils.GetEnv(\"SENSORS\")\n\tskipCollection := sensorsSet && sensorsEnvVal == \"\"\n\n\treturn a.newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal, skipCollection)\n}\n\n// Matches sensors.TemperaturesWithContext to allow for panic recovery (gopsutil/issues/1832)\ntype getTempsFn func(ctx context.Context) ([]sensors.TemperatureStat, error)\n\n// newSensorConfigWithEnv creates a SensorConfig with the provided environment variables\n// sensorsSet indicates if the SENSORS environment variable was explicitly set (even to empty string)\nfunc (a *Agent) newSensorConfigWithEnv(primarySensor, sysSensors, sensorsEnvVal string, skipCollection bool) *SensorConfig {\n\tconfig := &SensorConfig{\n\t\tcontext:        context.Background(),\n\t\tprimarySensor:  primarySensor,\n\t\tskipCollection: skipCollection,\n\t\tsensors:        make(map[string]struct{}),\n\t}\n\n\t// Set sensors context (allows overriding sys location for sensors)\n\tif sysSensors != \"\" {\n\t\tslog.Info(\"SYS_SENSORS\", \"path\", sysSensors)\n\t\tconfig.context = context.WithValue(config.context,\n\t\t\tcommon.EnvKey, common.EnvMap{common.HostSysEnvKey: sysSensors},\n\t\t)\n\t}\n\n\t// handle blacklist\n\tif strings.HasPrefix(sensorsEnvVal, \"-\") {\n\t\tconfig.isBlacklist = true\n\t\tsensorsEnvVal = sensorsEnvVal[1:]\n\t}\n\n\tfor sensor := range strings.SplitSeq(sensorsEnvVal, \",\") {\n\t\tsensor = strings.TrimSpace(sensor)\n\t\tif sensor != \"\" {\n\t\t\tconfig.sensors[sensor] = struct{}{}\n\t\t\tif strings.Contains(sensor, \"*\") {\n\t\t\t\tconfig.hasWildcards = true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn config\n}\n\n// updateTemperatures updates the agent with the latest sensor temperatures\nfunc (a *Agent) updateTemperatures(systemStats *system.Stats) {\n\t// skip if sensors whitelist is set to empty string\n\tif a.sensorConfig.skipCollection {\n\t\tslog.Debug(\"Skipping temperature collection\")\n\t\treturn\n\t}\n\n\t// reset high temp\n\ta.systemInfo.DashboardTemp = 0\n\n\ttemps, err := a.getTempsWithPanicRecovery(getSensorTemps)\n\tif err != nil {\n\t\t// retry once on panic (gopsutil/issues/1832)\n\t\ttemps, err = a.getTempsWithPanicRecovery(getSensorTemps)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"Error updating temperatures\", \"err\", err)\n\t\t\tif len(systemStats.Temperatures) > 0 {\n\t\t\t\tsystemStats.Temperatures = make(map[string]float64)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\tslog.Debug(\"Temperature\", \"sensors\", temps)\n\n\t// return if no sensors\n\tif len(temps) == 0 {\n\t\treturn\n\t}\n\n\tsystemStats.Temperatures = make(map[string]float64, len(temps))\n\tfor i, sensor := range temps {\n\t\t// check for malformed strings on darwin (gopsutil/issues/1832)\n\t\tif runtime.GOOS == \"darwin\" && !utf8.ValidString(sensor.SensorKey) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// scale temperature\n\t\tif sensor.Temperature != 0 && sensor.Temperature < 1 {\n\t\t\tsensor.Temperature = scaleTemperature(sensor.Temperature)\n\t\t}\n\t\t// skip if temperature is unreasonable\n\t\tif sensor.Temperature <= 0 || sensor.Temperature >= 200 {\n\t\t\tcontinue\n\t\t}\n\t\tsensorName := sensor.SensorKey\n\t\tif _, ok := systemStats.Temperatures[sensorName]; ok {\n\t\t\t// if key already exists, append int to key\n\t\t\tsensorName = sensorName + \"_\" + strconv.Itoa(i)\n\t\t}\n\t\t// skip if not in whitelist or blacklist\n\t\tif !isValidSensor(sensorName, a.sensorConfig) {\n\t\t\tcontinue\n\t\t}\n\t\t// set dashboard temperature\n\t\tswitch a.sensorConfig.primarySensor {\n\t\tcase \"\":\n\t\t\ta.systemInfo.DashboardTemp = max(a.systemInfo.DashboardTemp, sensor.Temperature)\n\t\tcase sensorName:\n\t\t\ta.systemInfo.DashboardTemp = sensor.Temperature\n\t\t}\n\t\tsystemStats.Temperatures[sensorName] = utils.TwoDecimals(sensor.Temperature)\n\t}\n}\n\n// getTempsWithPanicRecovery wraps sensors.TemperaturesWithContext to recover from panics (gopsutil/issues/1832)\nfunc (a *Agent) getTempsWithPanicRecovery(getTemps getTempsFn) (temps []sensors.TemperatureStat, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"panic: %v\", r)\n\t\t}\n\t}()\n\t// get sensor data (error ignored intentionally as it may be only with one sensor)\n\ttemps, _ = getTemps(a.sensorConfig.context)\n\treturn\n}\n\n// isValidSensor checks if a sensor is valid based on the sensor name and the sensor config\nfunc isValidSensor(sensorName string, config *SensorConfig) bool {\n\t// if no sensors configured, everything is valid\n\tif len(config.sensors) == 0 {\n\t\treturn true\n\t}\n\n\t// Exact match - return true if whitelist, false if blacklist\n\tif _, exactMatch := config.sensors[sensorName]; exactMatch {\n\t\treturn !config.isBlacklist\n\t}\n\n\t// If no wildcards, return true if blacklist, false if whitelist\n\tif !config.hasWildcards {\n\t\treturn config.isBlacklist\n\t}\n\n\t// Check for wildcard patterns\n\tfor pattern := range config.sensors {\n\t\tif !strings.Contains(pattern, \"*\") {\n\t\t\tcontinue\n\t\t}\n\t\tif match, _ := path.Match(pattern, sensorName); match {\n\t\t\treturn !config.isBlacklist\n\t\t}\n\t}\n\n\treturn config.isBlacklist\n}\n\n// scaleTemperature scales temperatures in fractional values to reasonable Celsius values\nfunc scaleTemperature(temp float64) float64 {\n\tif temp > 1 {\n\t\treturn temp\n\t}\n\tscaled100 := temp * 100\n\tscaled1000 := temp * 1000\n\n\tif scaled100 >= 15 && scaled100 <= 95 {\n\t\treturn scaled100\n\t} else if scaled1000 >= 15 && scaled1000 <= 95 {\n\t\treturn scaled1000\n\t}\n\treturn scaled100\n}\n"
  },
  {
    "path": "agent/sensors_default.go",
    "content": "//go:build !windows\n\npackage agent\n\nimport (\n\t\"github.com/shirou/gopsutil/v4/sensors\"\n)\n\nvar getSensorTemps = sensors.TemperaturesWithContext\n"
  },
  {
    "path": "agent/sensors_test.go",
    "content": "//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/entities/system\"\n\n\t\"github.com/shirou/gopsutil/v4/common\"\n\t\"github.com/shirou/gopsutil/v4/sensors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsValidSensor(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tsensorName    string\n\t\tconfig        *SensorConfig\n\t\texpectedValid bool\n\t}{\n\t\t{\n\t\t\tname:       \"Whitelist - sensor in list\",\n\t\t\tsensorName: \"cpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:     map[string]struct{}{\"cpu_temp\": {}},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Whitelist - sensor not in list\",\n\t\t\tsensorName: \"gpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:     map[string]struct{}{\"cpu_temp\": {}},\n\t\t\t\tisBlacklist: false,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist - sensor in list\",\n\t\t\tsensorName: \"cpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:     map[string]struct{}{\"cpu_temp\": {}},\n\t\t\t\tisBlacklist: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist - sensor not in list\",\n\t\t\tsensorName: \"gpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:     map[string]struct{}{\"cpu_temp\": {}},\n\t\t\t\tisBlacklist: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Whitelist with wildcard - matching pattern\",\n\t\t\tsensorName: \"core_0_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Whitelist with wildcard - non-matching pattern\",\n\t\t\tsensorName: \"gpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist with wildcard - matching pattern\",\n\t\t\tsensorName: \"core_0_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Blacklist with wildcard - non-matching pattern\",\n\t\t\tsensorName: \"gpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"No sensors configured\",\n\t\t\tsensorName: \"any_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:        map[string]struct{}{},\n\t\t\t\tisBlacklist:    false,\n\t\t\t\thasWildcards:   false,\n\t\t\t\tskipCollection: false,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Mixed patterns in whitelist - exact match\",\n\t\t\tsensorName: \"cpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"cpu_temp\": {}, \"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Mixed patterns in whitelist - wildcard match\",\n\t\t\tsensorName: \"core_1_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"cpu_temp\": {}, \"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Mixed patterns in blacklist - exact match\",\n\t\t\tsensorName: \"cpu_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"cpu_temp\": {}, \"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Mixed patterns in blacklist - wildcard match\",\n\t\t\tsensorName: \"core_1_temp\",\n\t\t\tconfig: &SensorConfig{\n\t\t\t\tsensors:      map[string]struct{}{\"cpu_temp\": {}, \"core_*_temp\": {}},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t\texpectedValid: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isValidSensor(tt.sensorName, tt.config)\n\t\t\tassert.Equal(t, tt.expectedValid, result, \"isValidSensor(%q, config) returned unexpected result\", tt.sensorName)\n\t\t})\n\t}\n}\n\nfunc TestNewSensorConfigWithEnv(t *testing.T) {\n\tagent := &Agent{}\n\n\ttests := []struct {\n\t\tname           string\n\t\tprimarySensor  string\n\t\tsysSensors     string\n\t\tsensors        string\n\t\tskipCollection bool\n\t\texpectedConfig *SensorConfig\n\t}{\n\t\t{\n\t\t\tname:          \"Empty configuration\",\n\t\t\tprimarySensor: \"\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:        context.Background(),\n\t\t\t\tprimarySensor:  \"\",\n\t\t\t\tsensors:        map[string]struct{}{},\n\t\t\t\tisBlacklist:    false,\n\t\t\t\thasWildcards:   false,\n\t\t\t\tskipCollection: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"Explicitly set to empty string\",\n\t\t\tprimarySensor:  \"\",\n\t\t\tsysSensors:     \"\",\n\t\t\tsensors:        \"\",\n\t\t\tskipCollection: true,\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:        context.Background(),\n\t\t\t\tprimarySensor:  \"\",\n\t\t\t\tsensors:        map[string]struct{}{},\n\t\t\t\tisBlacklist:    false,\n\t\t\t\thasWildcards:   false,\n\t\t\t\tskipCollection: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Primary sensor only - should create sensor map\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:       context.Background(),\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors:       map[string]struct{}{},\n\t\t\t\tisBlacklist:   false,\n\t\t\t\thasWildcards:  false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Whitelist sensors\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"cpu_temp,gpu_temp\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:       context.Background(),\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors: map[string]struct{}{\n\t\t\t\t\t\"cpu_temp\": {},\n\t\t\t\t\t\"gpu_temp\": {},\n\t\t\t\t},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Blacklist sensors\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"-cpu_temp,gpu_temp\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:       context.Background(),\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors: map[string]struct{}{\n\t\t\t\t\t\"cpu_temp\": {},\n\t\t\t\t\t\"gpu_temp\": {},\n\t\t\t\t},\n\t\t\t\tisBlacklist:  true,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Sensors with wildcard\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"cpu_*,gpu_temp\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:       context.Background(),\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors: map[string]struct{}{\n\t\t\t\t\t\"cpu_*\":    {},\n\t\t\t\t\t\"gpu_temp\": {},\n\t\t\t\t},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"Sensors with whitespace\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"\",\n\t\t\tsensors:       \"cpu_*, gpu_temp\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tcontext:       context.Background(),\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors: map[string]struct{}{\n\t\t\t\t\t\"cpu_*\":    {},\n\t\t\t\t\t\"gpu_temp\": {},\n\t\t\t\t},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"With SYS_SENSORS path\",\n\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\tsysSensors:    \"/custom/path\",\n\t\t\tsensors:       \"cpu_temp\",\n\t\t\texpectedConfig: &SensorConfig{\n\t\t\t\tprimarySensor: \"cpu_temp\",\n\t\t\t\tsensors: map[string]struct{}{\n\t\t\t\t\t\"cpu_temp\": {},\n\t\t\t\t},\n\t\t\t\tisBlacklist:  false,\n\t\t\t\thasWildcards: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.skipCollection)\n\n\t\t\t// Check primary sensor\n\t\t\tassert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor)\n\n\t\t\t// Check sensor map\n\t\t\tif tt.expectedConfig.sensors == nil {\n\t\t\t\tassert.Nil(t, result.sensors)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, len(tt.expectedConfig.sensors), len(result.sensors))\n\t\t\t\tfor sensor := range tt.expectedConfig.sensors {\n\t\t\t\t\t_, exists := result.sensors[sensor]\n\t\t\t\t\tassert.True(t, exists, \"Sensor %s should exist in the result\", sensor)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check flags\n\t\t\tassert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist)\n\t\t\tassert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards)\n\n\t\t\t// Check context\n\t\t\tif tt.sysSensors != \"\" {\n\t\t\t\t// Verify context contains correct values\n\t\t\t\tenvMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)\n\t\t\t\trequire.True(t, ok, \"Context should contain EnvMap\")\n\t\t\t\tsysPath, ok := envMap[common.HostSysEnvKey]\n\t\t\t\trequire.True(t, ok, \"EnvMap should contain HostSysEnvKey\")\n\t\t\t\tassert.Equal(t, tt.sysSensors, sysPath)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewSensorConfig(t *testing.T) {\n\t// Save original environment variables\n\toriginalPrimary, hasPrimary := os.LookupEnv(\"BESZEL_AGENT_PRIMARY_SENSOR\")\n\toriginalSys, hasSys := os.LookupEnv(\"BESZEL_AGENT_SYS_SENSORS\")\n\toriginalSensors, hasSensors := os.LookupEnv(\"BESZEL_AGENT_SENSORS\")\n\n\t// Restore environment variables after the test\n\tdefer func() {\n\t\t// Clean up test environment variables\n\t\tos.Unsetenv(\"BESZEL_AGENT_PRIMARY_SENSOR\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_SYS_SENSORS\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_SENSORS\")\n\n\t\t// Restore original values if they existed\n\t\tif hasPrimary {\n\t\t\tos.Setenv(\"BESZEL_AGENT_PRIMARY_SENSOR\", originalPrimary)\n\t\t}\n\t\tif hasSys {\n\t\t\tos.Setenv(\"BESZEL_AGENT_SYS_SENSORS\", originalSys)\n\t\t}\n\t\tif hasSensors {\n\t\t\tos.Setenv(\"BESZEL_AGENT_SENSORS\", originalSensors)\n\t\t}\n\t}()\n\n\t// Set test environment variables\n\tos.Setenv(\"BESZEL_AGENT_PRIMARY_SENSOR\", \"test_primary\")\n\tos.Setenv(\"BESZEL_AGENT_SYS_SENSORS\", \"/test/path\")\n\tos.Setenv(\"BESZEL_AGENT_SENSORS\", \"test_sensor1,test_*,test_sensor3\")\n\n\tagent := &Agent{}\n\tresult := agent.newSensorConfig()\n\n\t// Verify results\n\tassert.Equal(t, \"test_primary\", result.primarySensor)\n\tassert.NotNil(t, result.sensors)\n\tassert.Equal(t, 3, len(result.sensors))\n\tassert.True(t, result.hasWildcards)\n\tassert.False(t, result.isBlacklist)\n\n\t// Check that sys sensors path is in context\n\tenvMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)\n\trequire.True(t, ok, \"Context should contain EnvMap\")\n\tsysPath, ok := envMap[common.HostSysEnvKey]\n\trequire.True(t, ok, \"EnvMap should contain HostSysEnvKey\")\n\tassert.Equal(t, \"/test/path\", sysPath)\n}\n\nfunc TestScaleTemperature(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    float64\n\t\texpected float64\n\t\tdesc     string\n\t}{\n\t\t// Normal temperatures (no scaling needed)\n\t\t{\"normal_cpu_temp\", 45.0, 45.0, \"Normal CPU temperature\"},\n\t\t{\"normal_room_temp\", 25.0, 25.0, \"Normal room temperature\"},\n\t\t{\"high_cpu_temp\", 85.0, 85.0, \"High CPU temperature\"},\n\t\t// Zero temperature\n\t\t{\"zero_temp\", 0.0, 0.0, \"Zero temperature\"},\n\t\t// Fractional values that should use 100x scaling\n\t\t{\"fractional_45c\", 0.45, 45.0, \"0.45 should become 45°C (100x)\"},\n\t\t{\"fractional_25c\", 0.25, 25.0, \"0.25 should become 25°C (100x)\"},\n\t\t{\"fractional_60c\", 0.60, 60.0, \"0.60 should become 60°C (100x)\"},\n\t\t{\"fractional_75c\", 0.75, 75.0, \"0.75 should become 75°C (100x)\"},\n\t\t{\"fractional_30c\", 0.30, 30.0, \"0.30 should become 30°C (100x)\"},\n\t\t// Fractional values that should use 1000x scaling\n\t\t{\"millifractional_45c\", 0.045, 45.0, \"0.045 should become 45°C (1000x)\"},\n\t\t{\"millifractional_25c\", 0.025, 25.0, \"0.025 should become 25°C (1000x)\"},\n\t\t{\"millifractional_60c\", 0.060, 60.0, \"0.060 should become 60°C (1000x)\"},\n\t\t{\"millifractional_75c\", 0.075, 75.0, \"0.075 should become 75°C (1000x)\"},\n\t\t{\"millifractional_35c\", 0.035, 35.0, \"0.035 should become 35°C (1000x)\"},\n\t\t// Edge cases - values outside reasonable range\n\t\t{\"very_low_fractional\", 0.01, 1.0, \"0.01 should default to 100x scaling (1°C)\"},\n\t\t{\"very_high_fractional\", 0.99, 99.0, \"0.99 should default to 100x scaling (99°C)\"},\n\t\t{\"extremely_low\", 0.001, 0.1, \"0.001 should default to 100x scaling (0.1°C)\"},\n\t\t// Boundary cases around the reasonable range (15-95°C)\n\t\t{\"boundary_low_100x\", 0.15, 15.0, \"0.15 should use 100x scaling (15°C)\"},\n\t\t{\"boundary_high_100x\", 0.95, 95.0, \"0.95 should use 100x scaling (95°C)\"},\n\t\t{\"boundary_low_1000x\", 0.015, 15.0, \"0.015 should use 1000x scaling (15°C)\"},\n\t\t{\"boundary_high_1000x\", 0.095, 95.0, \"0.095 should use 1000x scaling (95°C)\"},\n\t\t// Values just outside reasonable range\n\t\t{\"just_below_range_100x\", 0.14, 14.0, \"0.14 should default to 100x (14°C)\"},\n\t\t{\"just_above_range_100x\", 0.96, 96.0, \"0.96 should default to 100x (96°C)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := scaleTemperature(tt.input)\n\t\t\tassert.InDelta(t, tt.expected, result, 0.001,\n\t\t\t\t\"scaleTemperature(%v) = %v, expected %v (%s)\",\n\t\t\t\ttt.input, result, tt.expected, tt.desc)\n\t\t})\n\t}\n}\n\nfunc TestScaleTemperatureLogic(t *testing.T) {\n\t// Test the logic flow for ambiguous cases\n\tt.Run(\"prefers_100x_when_both_valid\", func(t *testing.T) {\n\t\t// 0.5 could be 50°C (100x) or 500°C (1000x)\n\t\t// Should prefer 100x since it's tried first and is in range\n\t\tresult := scaleTemperature(0.5)\n\t\texpected := 50.0\n\t\tassert.InDelta(t, expected, result, 0.001,\n\t\t\t\"scaleTemperature(0.5) = %v, expected %v (should prefer 100x scaling)\",\n\t\t\tresult, expected)\n\t})\n\n\tt.Run(\"uses_1000x_when_100x_too_low\", func(t *testing.T) {\n\t\t// 0.05 -> 5°C (100x, too low) or 50°C (1000x, in range)\n\t\t// Should use 1000x since 100x is below reasonable range\n\t\tresult := scaleTemperature(0.05)\n\t\texpected := 50.0\n\t\tassert.InDelta(t, expected, result, 0.001,\n\t\t\t\"scaleTemperature(0.05) = %v, expected %v (should use 1000x scaling)\",\n\t\t\tresult, expected)\n\t})\n\n\tt.Run(\"defaults_to_100x_when_both_invalid\", func(t *testing.T) {\n\t\t// 0.005 -> 0.5°C (100x, too low) or 5°C (1000x, too low)\n\t\t// Should default to 100x scaling\n\t\tresult := scaleTemperature(0.005)\n\t\texpected := 0.5\n\t\tassert.InDelta(t, expected, result, 0.001,\n\t\t\t\"scaleTemperature(0.005) = %v, expected %v (should default to 100x)\",\n\t\t\tresult, expected)\n\t})\n}\n\nfunc TestGetTempsWithPanicRecovery(t *testing.T) {\n\tagent := &Agent{\n\t\tsystemInfo: system.Info{},\n\t\tsensorConfig: &SensorConfig{\n\t\t\tcontext: context.Background(),\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname        string\n\t\tgetTempsFn  getTempsFn\n\t\texpectError bool\n\t\terrorMsg    string\n\t}{\n\t\t{\n\t\t\tname: \"successful_function_call\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\treturn []sensors.TemperatureStat{\n\t\t\t\t\t{SensorKey: \"test_sensor\", Temperature: 45.0},\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"function_returns_error\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\treturn []sensors.TemperatureStat{\n\t\t\t\t\t{SensorKey: \"test_sensor\", Temperature: 45.0},\n\t\t\t\t}, fmt.Errorf(\"sensor error\")\n\t\t\t},\n\t\t\texpectError: false, // getTempsWithPanicRecovery ignores errors from the function\n\t\t},\n\t\t{\n\t\t\tname: \"function_panics_with_string\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\tpanic(\"test panic\")\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"panic: test panic\",\n\t\t},\n\t\t{\n\t\t\tname: \"function_panics_with_error\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\tpanic(fmt.Errorf(\"panic error\"))\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"panic:\",\n\t\t},\n\t\t{\n\t\t\tname: \"function_panics_with_index_out_of_bounds\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\tslice := []int{1, 2, 3}\n\t\t\t\t_ = slice[10] // out of bounds panic\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"panic:\",\n\t\t},\n\t\t{\n\t\t\tname: \"function_panics_with_any_conversion\",\n\t\t\tgetTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {\n\t\t\t\tvar i any = \"string\"\n\t\t\t\t_ = i.(int) // type assertion panic\n\t\t\t\treturn nil, nil\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"panic:\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar temps []sensors.TemperatureStat\n\t\t\tvar err error\n\n\t\t\t// The function should not panic, regardless of what the injected function does\n\t\t\tassert.NotPanics(t, func() {\n\t\t\t\ttemps, err = agent.getTempsWithPanicRecovery(tt.getTempsFn)\n\t\t\t}, \"getTempsWithPanicRecovery should not panic\")\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err, \"Expected an error to be returned\")\n\t\t\t\tif tt.errorMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errorMsg,\n\t\t\t\t\t\t\"Error message should contain expected text\")\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, temps, \"Temps should be nil when panic occurs\")\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, \"Should not return error for successful calls\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "agent/sensors_windows.go",
    "content": "//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\"context\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/shirou/gopsutil/v4/sensors\"\n)\n\n// Note: This is always called from Agent.gatherStats() which holds Agent.Lock(),\n// so no internal concurrency protection is needed.\n\n// lhmProcess is a wrapper around the LHM .NET process.\ntype lhmProcess struct {\n\tcmd                  *exec.Cmd\n\tstdin                io.WriteCloser\n\tstdout               io.ReadCloser\n\tscanner              *bufio.Scanner\n\tisRunning            bool\n\tstoppedNoSensors     bool\n\tconsecutiveNoSensors uint8\n\texecPath             string\n\ttempDir              string\n}\n\n//go:embed all:lhm/bin/Release/net48\nvar lhmFs embed.FS\n\nvar (\n\tbeszelLhm     *lhmProcess\n\tbeszelLhmOnce sync.Once\n\tuseLHM        = os.Getenv(\"LHM\") == \"true\"\n)\n\nvar errNoSensors = errors.New(\"no sensors found (try running as admin with LHM=true)\")\n\n// newlhmProcess copies the embedded LHM executable to a temporary directory and starts it.\nfunc newlhmProcess() (*lhmProcess, error) {\n\tdestDir := filepath.Join(os.TempDir(), \"beszel\")\n\texecPath := filepath.Join(destDir, \"beszel_lhm.exe\")\n\n\tif err := os.MkdirAll(destDir, 0755); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create temp directory: %w\", err)\n\t}\n\n\t// Only copy if executable doesn't exist\n\tif _, err := os.Stat(execPath); os.IsNotExist(err) {\n\t\tif err := copyEmbeddedDir(lhmFs, \"lhm/bin/Release/net48\", destDir); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to copy embedded directory: %w\", err)\n\t\t}\n\t}\n\n\tlhm := &lhmProcess{\n\t\texecPath: execPath,\n\t\ttempDir:  destDir,\n\t}\n\n\tif err := lhm.startProcess(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to start process: %w\", err)\n\t}\n\n\treturn lhm, nil\n}\n\n// startProcess starts the external LHM process\nfunc (lhm *lhmProcess) startProcess() error {\n\t// Clean up any existing process\n\tlhm.cleanupProcess()\n\n\tcmd := exec.Command(lhm.execPath)\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\tstdin.Close()\n\t\treturn err\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\tstdin.Close()\n\t\tstdout.Close()\n\t\treturn err\n\t}\n\n\t// Update process state\n\tlhm.cmd = cmd\n\tlhm.stdin = stdin\n\tlhm.stdout = stdout\n\tlhm.scanner = bufio.NewScanner(stdout)\n\tlhm.isRunning = true\n\n\t// Give process a moment to initialize\n\ttime.Sleep(100 * time.Millisecond)\n\n\treturn nil\n}\n\n// cleanupProcess terminates the process and closes resources but preserves files\nfunc (lhm *lhmProcess) cleanupProcess() {\n\tlhm.isRunning = false\n\n\tif lhm.cmd != nil && lhm.cmd.Process != nil {\n\t\tlhm.cmd.Process.Kill()\n\t\tlhm.cmd.Wait()\n\t}\n\n\tif lhm.stdin != nil {\n\t\tlhm.stdin.Close()\n\t\tlhm.stdin = nil\n\t}\n\tif lhm.stdout != nil {\n\t\tlhm.stdout.Close()\n\t\tlhm.stdout = nil\n\t}\n\n\tlhm.cmd = nil\n\tlhm.scanner = nil\n\tlhm.stoppedNoSensors = false\n\tlhm.consecutiveNoSensors = 0\n}\n\nfunc (lhm *lhmProcess) getTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {\n\tif !useLHM || lhm.stoppedNoSensors {\n\t\t// Fall back to gopsutil if we can't get sensors from LHM\n\t\treturn sensors.TemperaturesWithContext(ctx)\n\t}\n\n\t// Start process if it's not running\n\tif !lhm.isRunning || lhm.stdin == nil || lhm.scanner == nil {\n\t\terr := lhm.startProcess()\n\t\tif err != nil {\n\t\t\treturn temps, err\n\t\t}\n\t}\n\n\t// Send command to process\n\t_, err = fmt.Fprintln(lhm.stdin, \"getTemps\")\n\tif err != nil {\n\t\tlhm.isRunning = false\n\t\treturn temps, fmt.Errorf(\"failed to send command: %w\", err)\n\t}\n\n\t// Read all sensor lines until we hit an empty line or EOF\n\tfor lhm.scanner.Scan() {\n\t\tline := strings.TrimSpace(lhm.scanner.Text())\n\t\tif line == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tparts := strings.Split(line, \"|\")\n\t\tif len(parts) != 2 {\n\t\t\tslog.Debug(\"Invalid sensor format\", \"line\", line)\n\t\t\tcontinue\n\t\t}\n\n\t\tname := strings.TrimSpace(parts[0])\n\t\tvalueStr := strings.TrimSpace(parts[1])\n\n\t\tvalue, err := strconv.ParseFloat(valueStr, 64)\n\t\tif err != nil {\n\t\t\tslog.Debug(\"Failed to parse sensor\", \"err\", err, \"line\", line)\n\t\t\tcontinue\n\t\t}\n\n\t\tif name == \"\" || value <= 0 || value > 150 {\n\t\t\tslog.Debug(\"Invalid sensor\", \"name\", name, \"val\", value, \"line\", line)\n\t\t\tcontinue\n\t\t}\n\n\t\ttemps = append(temps, sensors.TemperatureStat{\n\t\t\tSensorKey:   name,\n\t\t\tTemperature: value,\n\t\t})\n\t}\n\n\tif err := lhm.scanner.Err(); err != nil {\n\t\tlhm.isRunning = false\n\t\treturn temps, err\n\t}\n\n\t// Handle no sensors case\n\tif len(temps) == 0 {\n\t\tlhm.consecutiveNoSensors++\n\t\tif lhm.consecutiveNoSensors >= 3 {\n\t\t\tlhm.stoppedNoSensors = true\n\t\t\tslog.Warn(errNoSensors.Error())\n\t\t\tlhm.cleanup()\n\t\t}\n\t\treturn sensors.TemperaturesWithContext(ctx)\n\t}\n\n\tlhm.consecutiveNoSensors = 0\n\n\treturn temps, nil\n}\n\n// getSensorTemps attempts to pull sensor temperatures from the embedded LHM process.\n// NB: LibreHardwareMonitorLib requires admin privileges to access all available sensors.\nfunc getSensorTemps(ctx context.Context) (temps []sensors.TemperatureStat, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tslog.Debug(\"Error reading sensors\", \"err\", err)\n\t\t}\n\t}()\n\n\tif !useLHM {\n\t\treturn sensors.TemperaturesWithContext(ctx)\n\t}\n\n\t// Initialize process once\n\tbeszelLhmOnce.Do(func() {\n\t\tbeszelLhm, err = newlhmProcess()\n\t})\n\n\tif err != nil {\n\t\treturn temps, fmt.Errorf(\"failed to initialize lhm: %w\", err)\n\t}\n\n\tif beszelLhm == nil {\n\t\treturn temps, fmt.Errorf(\"lhm not available\")\n\t}\n\n\treturn beszelLhm.getTemps(ctx)\n}\n\n// cleanup terminates the process and closes resources\nfunc (lhm *lhmProcess) cleanup() {\n\tlhm.cleanupProcess()\n\tif lhm.tempDir != \"\" {\n\t\tos.RemoveAll(lhm.tempDir)\n\t}\n}\n\n// copyEmbeddedDir copies the embedded directory to the destination path\nfunc copyEmbeddedDir(fs embed.FS, srcPath, destPath string) error {\n\tentries, err := fs.ReadDir(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(destPath, 0755); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, entry := range entries {\n\t\tsrcEntryPath := path.Join(srcPath, entry.Name())\n\t\tdestEntryPath := filepath.Join(destPath, entry.Name())\n\n\t\tif entry.IsDir() {\n\t\t\tif err := copyEmbeddedDir(fs, srcEntryPath, destEntryPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, err := fs.ReadFile(srcEntryPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := os.WriteFile(destEntryPath, data, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "agent/server.go",
    "content": "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.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gliderlabs/ssh\"\n\tgossh \"golang.org/x/crypto/ssh\"\n)\n\n// ServerOptions contains configuration options for starting the SSH server.\ntype ServerOptions struct {\n\tAddr    string            // Network address to listen on (e.g., \":45876\" or \"/path/to/socket\")\n\tNetwork string            // Network type (\"tcp\" or \"unix\")\n\tKeys    []gossh.PublicKey // SSH public keys for authentication\n}\n\n// hubVersions caches hub versions by session ID to avoid repeated parsing.\nvar hubVersions map[string]semver.Version\n\n// StartServer starts the SSH server with the provided options.\n// It configures the server with secure defaults, sets up authentication,\n// and begins listening for connections. Returns an error if the server\n// is already running or if there's an issue starting the server.\nfunc (a *Agent) StartServer(opts ServerOptions) error {\n\tif disableSSH, _ := utils.GetEnv(\"DISABLE_SSH\"); disableSSH == \"true\" {\n\t\treturn errors.New(\"SSH disabled\")\n\t}\n\tif a.server != nil {\n\t\treturn errors.New(\"server already started\")\n\t}\n\n\tslog.Info(\"Starting SSH server\", \"addr\", opts.Addr, \"network\", opts.Network)\n\n\tif opts.Network == \"unix\" {\n\t\t// remove existing socket file if it exists\n\t\tif err := os.Remove(opts.Addr); err != nil && !os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// start listening on the address\n\tln, err := net.Listen(opts.Network, opts.Addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer ln.Close()\n\n\t// base config (limit to allowed algorithms)\n\tconfig := &gossh.ServerConfig{\n\t\tServerVersion: fmt.Sprintf(\"SSH-2.0-%s_%s\", beszel.AppName, beszel.Version),\n\t}\n\tconfig.KeyExchanges = common.DefaultKeyExchanges\n\tconfig.MACs = common.DefaultMACs\n\tconfig.Ciphers = common.DefaultCiphers\n\n\t// set default handler\n\tssh.Handle(a.handleSession)\n\n\ta.server = &ssh.Server{\n\t\tServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {\n\t\t\treturn config\n\t\t},\n\t\t// check public key(s)\n\t\tPublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {\n\t\t\tremoteAddr := ctx.RemoteAddr()\n\t\t\tfor _, pubKey := range opts.Keys {\n\t\t\t\tif ssh.KeysEqual(key, pubKey) {\n\t\t\t\t\tslog.Info(\"SSH connected\", \"addr\", remoteAddr)\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\tslog.Warn(\"Invalid SSH key\", \"addr\", remoteAddr)\n\t\t\treturn false\n\t\t},\n\t\t// disable pty\n\t\tPtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {\n\t\t\treturn false\n\t\t},\n\t\t// close idle connections after 70 seconds\n\t\tIdleTimeout: 70 * time.Second,\n\t}\n\n\t// Start SSH server on the listener\n\treturn a.server.Serve(ln)\n}\n\n// getHubVersion retrieves and caches the hub version for a given session.\n// It extracts the version from the SSH client version string and caches\n// it to avoid repeated parsing. Returns a zero version if parsing fails.\nfunc (a *Agent) getHubVersion(sessionId string, sessionCtx ssh.Context) semver.Version {\n\tif hubVersions == nil {\n\t\thubVersions = make(map[string]semver.Version, 1)\n\t}\n\thubVersion, ok := hubVersions[sessionId]\n\tif ok {\n\t\treturn hubVersion\n\t}\n\t// Extract hub version from SSH client version\n\tclientVersion := sessionCtx.Value(ssh.ContextKeyClientVersion)\n\tif versionStr, ok := clientVersion.(string); ok {\n\t\thubVersion, _ = extractHubVersion(versionStr)\n\t}\n\thubVersions[sessionId] = hubVersion\n\treturn hubVersion\n}\n\n// handleSession handles an incoming SSH session by gathering system statistics\n// and sending them to the hub. It signals connection events, determines the\n// appropriate encoding format based on hub version, and exits with appropriate\n// status codes.\nfunc (a *Agent) handleSession(s ssh.Session) {\n\ta.connectionManager.eventChan <- SSHConnect\n\n\tsessionCtx := s.Context()\n\tsessionID := sessionCtx.SessionID()\n\n\thubVersion := a.getHubVersion(sessionID, sessionCtx)\n\n\t// Legacy one-shot behavior for older hubs\n\tif hubVersion.LT(beszel.MinVersionAgentResponse) {\n\t\tif err := a.handleLegacyStats(s, hubVersion); err != nil {\n\t\t\tslog.Error(\"Error encoding stats\", \"err\", err)\n\t\t\ts.Exit(1)\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar req common.HubRequest[cbor.RawMessage]\n\tif err := cbor.NewDecoder(s).Decode(&req); err != nil {\n\t\t// Fallback to legacy one-shot if the first decode fails\n\t\tif err2 := a.handleLegacyStats(s, hubVersion); err2 != nil {\n\t\t\tslog.Error(\"Error encoding stats (fallback)\", \"err\", err2)\n\t\t\ts.Exit(1)\n\t\t\treturn\n\t\t}\n\t\ts.Exit(0)\n\t\treturn\n\t}\n\tif err := a.handleSSHRequest(s, &req); err != nil {\n\t\tslog.Error(\"SSH request handling failed\", \"err\", err)\n\t\ts.Exit(1)\n\t\treturn\n\t}\n\ts.Exit(0)\n}\n\n// handleSSHRequest builds a handler context and dispatches to the shared registry\nfunc (a *Agent) handleSSHRequest(w io.Writer, req *common.HubRequest[cbor.RawMessage]) error {\n\t// SSH does not support fingerprint auth action\n\tif req.Action == common.CheckFingerprint {\n\t\treturn cbor.NewEncoder(w).Encode(common.AgentResponse{Error: \"unsupported action\"})\n\t}\n\n\t// responder that writes AgentResponse to stdout\n\t// Uses legacy typed fields for backward compatibility with <= 0.17\n\tsshResponder := func(data any, requestID *uint32) error {\n\t\tresponse := newAgentResponse(data, requestID)\n\t\treturn cbor.NewEncoder(w).Encode(response)\n\t}\n\n\tctx := &HandlerContext{\n\t\tClient:       nil,\n\t\tAgent:        a,\n\t\tRequest:      req,\n\t\tRequestID:    nil,\n\t\tHubVerified:  true,\n\t\tSendResponse: sshResponder,\n\t}\n\n\tif handler, ok := a.handlerRegistry.GetHandler(req.Action); ok {\n\t\tif err := handler.Handle(ctx); err != nil {\n\t\t\treturn cbor.NewEncoder(w).Encode(common.AgentResponse{Error: err.Error()})\n\t\t}\n\t\treturn nil\n\t}\n\treturn cbor.NewEncoder(w).Encode(common.AgentResponse{Error: fmt.Sprintf(\"unknown action: %d\", req.Action)})\n}\n\n// handleLegacyStats serves the legacy one-shot stats payload for older hubs\nfunc (a *Agent) handleLegacyStats(w io.Writer, hubVersion semver.Version) error {\n\tstats := a.gatherStats(common.DataRequestOptions{CacheTimeMs: 60_000})\n\treturn a.writeToSession(w, stats, hubVersion)\n}\n\n// writeToSession encodes and writes system statistics to the session.\n// It chooses between CBOR and JSON encoding based on the hub version,\n// using CBOR for newer versions and JSON for legacy compatibility.\nfunc (a *Agent) writeToSession(w io.Writer, stats *system.CombinedData, hubVersion semver.Version) error {\n\tif hubVersion.GTE(beszel.MinVersionCbor) {\n\t\treturn cbor.NewEncoder(w).Encode(stats)\n\t}\n\treturn json.NewEncoder(w).Encode(stats)\n}\n\n// extractHubVersion extracts the beszel version from SSH client version string.\n// Expected format: \"SSH-2.0-beszel_X.Y.Z\" or \"beszel_X.Y.Z\"\nfunc extractHubVersion(versionString string) (semver.Version, error) {\n\t_, after, _ := strings.Cut(versionString, \"_\")\n\treturn semver.Parse(after)\n}\n\n// ParseKeys parses a string containing SSH public keys in authorized_keys format.\n// It returns a slice of ssh.PublicKey and an error if any key fails to parse.\nfunc ParseKeys(input string) ([]gossh.PublicKey, error) {\n\tvar parsedKeys []gossh.PublicKey\n\tfor line := range strings.Lines(input) {\n\t\tline = strings.TrimSpace(line)\n\t\t// Skip empty lines or comments\n\t\tif len(line) == 0 || strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\t\t// Parse the key\n\t\tparsedKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(line))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse key: %s, error: %w\", line, err)\n\t\t}\n\t\tparsedKeys = append(parsedKeys, parsedKey)\n\t}\n\treturn parsedKeys, nil\n}\n\n// GetAddress determines the network address to listen on from various sources.\n// It checks the provided address, then environment variables (LISTEN, PORT),\n// and finally defaults to \":45876\".\nfunc GetAddress(addr string) string {\n\tif addr == \"\" {\n\t\taddr, _ = utils.GetEnv(\"LISTEN\")\n\t}\n\tif addr == \"\" {\n\t\t// Legacy PORT environment variable support\n\t\taddr, _ = utils.GetEnv(\"PORT\")\n\t}\n\tif addr == \"\" {\n\t\treturn \":45876\"\n\t}\n\t// prefix with : if only port was provided\n\tif GetNetwork(addr) != \"unix\" && !strings.Contains(addr, \":\") {\n\t\taddr = \":\" + addr\n\t}\n\treturn addr\n}\n\n// GetNetwork determines the network type based on the address format.\n// It checks the NETWORK environment variable first, then infers from\n// the address format: addresses starting with \"/\" are \"unix\", others are \"tcp\".\nfunc GetNetwork(addr string) string {\n\tif network, ok := utils.GetEnv(\"NETWORK\"); ok && network != \"\" {\n\t\treturn network\n\t}\n\tif strings.HasPrefix(addr, \"/\") {\n\t\treturn \"unix\"\n\t}\n\treturn \"tcp\"\n}\n\n// StopServer stops the SSH server if it's running.\n// It returns an error if the server is not running or if there's an error stopping it.\nfunc (a *Agent) StopServer() error {\n\tif a.server == nil {\n\t\treturn errors.New(\"SSH server not running\")\n\t}\n\n\tslog.Info(\"Stopping SSH server\")\n\t_ = a.server.Close()\n\ta.server = nil\n\ta.connectionManager.eventChan <- SSHDisconnect\n\treturn nil\n}\n"
  },
  {
    "path": "agent/server_test.go",
    "content": "//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/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gliderlabs/ssh\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tgossh \"golang.org/x/crypto/ssh\"\n)\n\nfunc TestStartServer(t *testing.T) {\n\t// Generate a test key pair\n\tpubKey, privKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tsigner, err := gossh.NewSignerFromKey(privKey)\n\trequire.NoError(t, err)\n\tsshPubKey, err := gossh.NewPublicKey(pubKey)\n\trequire.NoError(t, err)\n\n\t// Generate a different key pair for bad key test\n\tbadPubKey, badPrivKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tbadSigner, err := gossh.NewSignerFromKey(badPrivKey)\n\trequire.NoError(t, err)\n\tsshBadPubKey, err := gossh.NewPublicKey(badPubKey)\n\trequire.NoError(t, err)\n\n\tsocketFile := filepath.Join(t.TempDir(), \"beszel-test.sock\")\n\n\ttests := []struct {\n\t\tname        string\n\t\tconfig      ServerOptions\n\t\twantErr     bool\n\t\terrContains string\n\t\tsetup       func() error\n\t\tcleanup     func() error\n\t}{\n\t\t{\n\t\t\tname: \"tcp port only\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"tcp\",\n\t\t\t\tAddr:    \":45987\",\n\t\t\t\tKeys:    []gossh.PublicKey{sshPubKey},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tcp with ipv4\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"tcp4\",\n\t\t\t\tAddr:    \"127.0.0.1:45988\",\n\t\t\t\tKeys:    []gossh.PublicKey{sshPubKey},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tcp with ipv6\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"tcp6\",\n\t\t\t\tAddr:    \"[::1]:45989\",\n\t\t\t\tKeys:    []gossh.PublicKey{sshPubKey},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"unix socket\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"unix\",\n\t\t\t\tAddr:    socketFile,\n\t\t\t\tKeys:    []gossh.PublicKey{sshPubKey},\n\t\t\t},\n\t\t\tsetup: func() error {\n\t\t\t\t// Create a socket file that should be removed\n\t\t\t\tf, err := os.Create(socketFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn f.Close()\n\t\t\t},\n\t\t\tcleanup: func() error {\n\t\t\t\treturn os.Remove(socketFile)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bad key should fail\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"tcp\",\n\t\t\t\tAddr:    \":45987\",\n\t\t\t\tKeys:    []gossh.PublicKey{sshBadPubKey},\n\t\t\t},\n\t\t\twantErr:     true,\n\t\t\terrContains: \"ssh: handshake failed\",\n\t\t},\n\t\t{\n\t\t\tname: \"good key still good\",\n\t\t\tconfig: ServerOptions{\n\t\t\t\tNetwork: \"tcp\",\n\t\t\t\tAddr:    \":45987\",\n\t\t\t\tKeys:    []gossh.PublicKey{sshPubKey},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.setup != nil {\n\t\t\t\terr := tt.setup()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tif tt.cleanup != nil {\n\t\t\t\tdefer tt.cleanup()\n\t\t\t}\n\n\t\t\tagent, err := NewAgent(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Start server in a goroutine since it blocks\n\t\t\terrChan := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\terrChan <- agent.StartServer(tt.config)\n\t\t\t}()\n\n\t\t\t// Add a short delay to allow the server to start\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t// Try to connect to verify server is running\n\t\t\tvar client *gossh.Client\n\n\t\t\t// Choose the appropriate signer based on the test case\n\t\t\ttestSigner := signer\n\t\t\tif tt.name == \"bad key should fail\" {\n\t\t\t\ttestSigner = badSigner\n\t\t\t}\n\n\t\t\tsshClientConfig := &gossh.ClientConfig{\n\t\t\t\tUser: \"a\",\n\t\t\t\tAuth: []gossh.AuthMethod{\n\t\t\t\t\tgossh.PublicKeys(testSigner),\n\t\t\t\t},\n\t\t\t\tHostKeyCallback: gossh.InsecureIgnoreHostKey(),\n\t\t\t\tTimeout:         4 * time.Second,\n\t\t\t}\n\n\t\t\tswitch tt.config.Network {\n\t\t\tcase \"unix\":\n\t\t\t\tclient, err = gossh.Dial(\"unix\", tt.config.Addr, sshClientConfig)\n\t\t\tdefault:\n\t\t\t\tif !strings.Contains(tt.config.Addr, \":\") {\n\t\t\t\t\ttt.config.Addr = \":\" + tt.config.Addr\n\t\t\t\t}\n\t\t\t\tclient, err = gossh.Dial(\"tcp\", tt.config.Addr, sshClientConfig)\n\t\t\t}\n\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tif tt.errContains != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errContains)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, client)\n\t\t\tclient.Close()\n\t\t})\n\t}\n}\n\nfunc TestStartServerDisableSSH(t *testing.T) {\n\tos.Setenv(\"BESZEL_AGENT_DISABLE_SSH\", \"true\")\n\tdefer os.Unsetenv(\"BESZEL_AGENT_DISABLE_SSH\")\n\n\tagent, err := NewAgent(\"\")\n\trequire.NoError(t, err)\n\n\topts := ServerOptions{\n\t\tNetwork: \"tcp\",\n\t\tAddr:    \":45990\",\n\t}\n\n\terr = agent.StartServer(opts)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"SSH disabled\")\n}\n\n/////////////////////////////////////////////////////////////////\n//////////////////// ParseKeys Tests ////////////////////////////\n/////////////////////////////////////////////////////////////////\n\n// Helper function to generate a temporary file with content\nfunc createTempFile(content string) (string, error) {\n\ttmpFile, err := os.CreateTemp(\"\", \"ssh_keys_*.txt\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create temp file: %w\", err)\n\t}\n\tdefer tmpFile.Close()\n\n\tif _, err := tmpFile.WriteString(content); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to write to temp file: %w\", err)\n\t}\n\n\treturn tmpFile.Name(), nil\n}\n\n// Test case 1: String with a single SSH key\nfunc TestParseSingleKeyFromString(t *testing.T) {\n\tinput := \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\"\n\tkeys, err := ParseKeys(input)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got: %v\", err)\n\t}\n\tif len(keys) != 1 {\n\t\tt.Fatalf(\"Expected 1 key, got %d keys\", len(keys))\n\t}\n\tif keys[0].Type() != \"ssh-ed25519\" {\n\t\tt.Fatalf(\"Expected key type 'ssh-ed25519', got '%s'\", keys[0].Type())\n\t}\n}\n\n// Test case 2: String with multiple SSH keys\nfunc TestParseMultipleKeysFromString(t *testing.T) {\n\tinput := \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D \\n #comment\\n ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D\"\n\tkeys, err := ParseKeys(input)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got: %v\", err)\n\t}\n\tif len(keys) != 3 {\n\t\tt.Fatalf(\"Expected 3 keys, got %d keys\", len(keys))\n\t}\n\tif keys[0].Type() != \"ssh-ed25519\" || keys[1].Type() != \"ssh-ed25519\" || keys[2].Type() != \"ssh-ed25519\" {\n\t\tt.Fatalf(\"Unexpected key types: %s, %s, %s\", keys[0].Type(), keys[1].Type(), keys[2].Type())\n\t}\n}\n\n// Test case 3: File with a single SSH key\nfunc TestParseSingleKeyFromFile(t *testing.T) {\n\tcontent := \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\"\n\tfilePath, err := createTempFile(content)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp file: %v\", err)\n\t}\n\tdefer os.Remove(filePath) // Clean up the file after the test\n\n\t// Read the file content\n\tfileContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to read temp file: %v\", err)\n\t}\n\n\t// Parse the keys\n\tkeys, err := ParseKeys(string(fileContent))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got: %v\", err)\n\t}\n\tif len(keys) != 1 {\n\t\tt.Fatalf(\"Expected 1 key, got %d keys\", len(keys))\n\t}\n\tif keys[0].Type() != \"ssh-ed25519\" {\n\t\tt.Fatalf(\"Expected key type 'ssh-ed25519', got '%s'\", keys[0].Type())\n\t}\n}\n\n// Test case 4: File with multiple SSH keys\nfunc TestParseMultipleKeysFromFile(t *testing.T) {\n\tcontent := \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKCBM91kukN7hbvFKtbpEeo2JXjCcNxXcdBH7V7ADMBo\\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D \\n #comment\\n ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJDMtAOQfxDlCxe+A5lVbUY/DHxK1LAF2Z3AV0FYv36D\"\n\tfilePath, err := createTempFile(content)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp file: %v\", err)\n\t}\n\t// defer os.Remove(filePath) // Clean up the file after the test\n\n\t// Read the file content\n\tfileContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to read temp file: %v\", err)\n\t}\n\n\t// Parse the keys\n\tkeys, err := ParseKeys(string(fileContent))\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got: %v\", err)\n\t}\n\tif len(keys) != 3 {\n\t\tt.Fatalf(\"Expected 3 keys, got %d keys\", len(keys))\n\t}\n\tif keys[0].Type() != \"ssh-ed25519\" || keys[1].Type() != \"ssh-ed25519\" || keys[2].Type() != \"ssh-ed25519\" {\n\t\tt.Fatalf(\"Unexpected key types: %s, %s, %s\", keys[0].Type(), keys[1].Type(), keys[2].Type())\n\t}\n}\n\n// Test case 5: Invalid SSH key input\nfunc TestParseInvalidKey(t *testing.T) {\n\tinput := \"invalid-key-data\"\n\t_, err := ParseKeys(input)\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error for invalid key, got nil\")\n\t}\n\texpectedErrMsg := \"failed to parse key\"\n\tif !strings.Contains(err.Error(), expectedErrMsg) {\n\t\tt.Fatalf(\"Expected error message to contain '%s', got: %v\", expectedErrMsg, err)\n\t}\n}\n\n/////////////////////////////////////////////////////////////////\n//////////////////// Hub Version Tests //////////////////////////\n/////////////////////////////////////////////////////////////////\n\nfunc TestExtractHubVersion(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tclientVersion   string\n\t\texpectedVersion string\n\t\texpectError     bool\n\t}{\n\t\t{\n\t\t\tname:            \"valid beszel client version with underscore\",\n\t\t\tclientVersion:   \"SSH-2.0-beszel_0.11.1\",\n\t\t\texpectedVersion: \"0.11.1\",\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"valid beszel client version with beta\",\n\t\t\tclientVersion:   \"SSH-2.0-beszel_1.0.0-beta\",\n\t\t\texpectedVersion: \"1.0.0-beta\",\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"valid beszel client version with rc\",\n\t\t\tclientVersion:   \"SSH-2.0-beszel_0.12.0-rc1\",\n\t\t\texpectedVersion: \"0.12.0-rc1\",\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"different SSH client\",\n\t\t\tclientVersion:   \"SSH-2.0-OpenSSH_8.0\",\n\t\t\texpectedVersion: \"8.0\",\n\t\t\texpectError:     true,\n\t\t},\n\t\t{\n\t\t\tname:          \"malformed version string without underscore\",\n\t\t\tclientVersion: \"SSH-2.0-beszel\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"empty version string\",\n\t\t\tclientVersion: \"\",\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:            \"version string with underscore but no version\",\n\t\t\tclientVersion:   \"beszel_\",\n\t\t\texpectedVersion: \"\",\n\t\t\texpectError:     true,\n\t\t},\n\t\t{\n\t\t\tname:            \"version with patch and build metadata\",\n\t\t\tclientVersion:   \"SSH-2.0-beszel_1.2.3+build.123\",\n\t\t\texpectedVersion: \"1.2.3+build.123\",\n\t\t\texpectError:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := extractHubVersion(tt.clientVersion)\n\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expectedVersion, result.String())\n\t\t})\n\t}\n}\n\n/////////////////////////////////////////////////////////////////\n/////////////// Hub Version Detection Tests ////////////////////\n/////////////////////////////////////////////////////////////////\n\nfunc TestGetHubVersion(t *testing.T) {\n\tagent, err := NewAgent(\"\")\n\trequire.NoError(t, err)\n\n\t// Mock SSH context that implements the ssh.Context interface\n\tmockCtx := &mockSSHContext{\n\t\tsessionID:     \"test-session-123\",\n\t\tclientVersion: \"SSH-2.0-beszel_0.12.0\",\n\t}\n\n\t// Test first call - should extract and cache version\n\tversion := agent.getHubVersion(\"test-session-123\", mockCtx)\n\tassert.Equal(t, \"0.12.0\", version.String())\n\n\t// Test second call - should return cached version\n\tmockCtx.clientVersion = \"SSH-2.0-beszel_0.11.0\" // Change version but should still return cached\n\tversion = agent.getHubVersion(\"test-session-123\", mockCtx)\n\tassert.Equal(t, \"0.12.0\", version.String()) // Should still be cached version\n\n\t// Test different session - should extract new version\n\tversion = agent.getHubVersion(\"different-session\", mockCtx)\n\tassert.Equal(t, \"0.11.0\", version.String())\n\n\t// Test with invalid version string (non-beszel client)\n\tmockCtx.clientVersion = \"SSH-2.0-OpenSSH_8.0\"\n\tversion = agent.getHubVersion(\"invalid-session\", mockCtx)\n\tassert.Equal(t, \"0.0.0\", version.String()) // Should be empty version for non-beszel clients\n\n\t// Test with no client version\n\tmockCtx.clientVersion = \"\"\n\tversion = agent.getHubVersion(\"no-version-session\", mockCtx)\n\tassert.True(t, version.EQ(semver.Version{})) // Should be empty version\n}\n\n// mockSSHContext implements ssh.Context for testing\ntype mockSSHContext struct {\n\tcontext.Context\n\tsync.Mutex\n\tsessionID     string\n\tclientVersion string\n}\n\nfunc (m *mockSSHContext) SessionID() string {\n\treturn m.sessionID\n}\n\nfunc (m *mockSSHContext) ClientVersion() string {\n\treturn m.clientVersion\n}\n\nfunc (m *mockSSHContext) ServerVersion() string {\n\treturn \"SSH-2.0-beszel_test\"\n}\n\nfunc (m *mockSSHContext) Value(key interface{}) interface{} {\n\tif key == ssh.ContextKeyClientVersion {\n\t\treturn m.clientVersion\n\t}\n\treturn nil\n}\n\nfunc (m *mockSSHContext) User() string                    { return \"test-user\" }\nfunc (m *mockSSHContext) RemoteAddr() net.Addr            { return nil }\nfunc (m *mockSSHContext) LocalAddr() net.Addr             { return nil }\nfunc (m *mockSSHContext) Permissions() *ssh.Permissions   { return nil }\nfunc (m *mockSSHContext) SetValue(key, value interface{}) {}\n\n/////////////////////////////////////////////////////////////////\n/////////////// CBOR vs JSON Encoding Tests ////////////////////\n/////////////////////////////////////////////////////////////////\n\n// TestWriteToSessionEncoding tests that writeToSession actually encodes data in the correct format\nfunc TestWriteToSessionEncoding(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\thubVersion       string\n\t\texpectedUsesCbor bool\n\t}{\n\t\t{\n\t\t\tname:             \"old hub version should use JSON\",\n\t\t\thubVersion:       \"0.11.1\",\n\t\t\texpectedUsesCbor: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"non-beta release should use CBOR\",\n\t\t\thubVersion:       \"0.12.0\",\n\t\t\texpectedUsesCbor: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"even newer hub version should use CBOR\",\n\t\t\thubVersion:       \"0.16.4\",\n\t\t\texpectedUsesCbor: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"beta version below release threshold should use JSON\",\n\t\t\thubVersion:       \"0.12.0-beta0\",\n\t\t\texpectedUsesCbor: false,\n\t\t},\n\t\t// {\n\t\t// \tname:             \"matching beta version should use CBOR\",\n\t\t// \thubVersion:       \"0.12.0-beta2\",\n\t\t// \texpectedUsesCbor: true,\n\t\t// },\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Reset the global hubVersions map to ensure clean state for each test\n\t\t\thubVersions = nil\n\n\t\t\tagent, err := NewAgent(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Parse the test version\n\t\t\tversion, err := semver.Parse(tt.hubVersion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create test data to encode\n\t\t\ttestData := createTestCombinedData()\n\n\t\t\tvar buf strings.Builder\n\t\t\terr = agent.writeToSession(&buf, testData, version)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tencodedData := buf.String()\n\t\t\trequire.NotEmpty(t, encodedData)\n\n\t\t\t// Verify the encoding format by attempting to decode\n\t\t\tif tt.expectedUsesCbor {\n\t\t\t\tvar decodedCbor system.CombinedData\n\t\t\t\terr = cbor.Unmarshal([]byte(encodedData), &decodedCbor)\n\t\t\t\tassert.NoError(t, err, \"Should be valid CBOR data\")\n\n\t\t\t\tvar decodedJson system.CombinedData\n\t\t\t\terr = json.Unmarshal([]byte(encodedData), &decodedJson)\n\t\t\t\tassert.Error(t, err, \"Should not be valid JSON data\")\n\n\t\t\t\tassert.Equal(t, testData.Details.Hostname, decodedCbor.Details.Hostname)\n\t\t\t\tassert.Equal(t, testData.Stats.Cpu, decodedCbor.Stats.Cpu)\n\t\t\t} else {\n\t\t\t\t// Should be JSON - try to decode as JSON\n\t\t\t\tvar decodedJson system.CombinedData\n\t\t\t\terr = json.Unmarshal([]byte(encodedData), &decodedJson)\n\t\t\t\tassert.NoError(t, err, \"Should be valid JSON data\")\n\n\t\t\t\tvar decodedCbor system.CombinedData\n\t\t\t\terr = cbor.Unmarshal([]byte(encodedData), &decodedCbor)\n\t\t\t\tassert.Error(t, err, \"Should not be valid CBOR data\")\n\n\t\t\t\t// Verify the decoded JSON data matches our test data\n\t\t\t\tassert.Equal(t, testData.Details.Hostname, decodedJson.Details.Hostname)\n\t\t\t\tassert.Equal(t, testData.Stats.Cpu, decodedJson.Stats.Cpu)\n\n\t\t\t\t// Verify it looks like JSON (starts with '{' and contains readable field names)\n\t\t\t\tassert.True(t, strings.HasPrefix(encodedData, \"{\"), \"JSON should start with '{'\")\n\t\t\t\tassert.Contains(t, encodedData, `\"info\"`, \"JSON should contain readable field names\")\n\t\t\t\tassert.Contains(t, encodedData, `\"stats\"`, \"JSON should contain readable field names\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function to create test data for encoding tests\nfunc createTestCombinedData() *system.CombinedData {\n\treturn &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tCpu:       25.5,\n\t\t\tMem:       8589934592, // 8GB\n\t\t\tMemUsed:   4294967296, // 4GB\n\t\t\tMemPct:    50.0,\n\t\t\tDiskTotal: 1099511627776, // 1TB\n\t\t\tDiskUsed:  549755813888,  // 512GB\n\t\t\tDiskPct:   50.0,\n\t\t},\n\t\tDetails: &system.Details{\n\t\t\tHostname: \"test-host\",\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tUptime:       3600,\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t},\n\t\tContainers: []*container.Stats{\n\t\t\t{\n\t\t\t\tName: \"test-container\",\n\t\t\t\tCpu:  10.5,\n\t\t\t\tMem:  1073741824, // 1GB\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestHubVersionCaching(t *testing.T) {\n\t// Reset the global hubVersions map to ensure clean state\n\thubVersions = nil\n\n\tagent, err := NewAgent(\"\")\n\trequire.NoError(t, err)\n\n\tctx1 := &mockSSHContext{\n\t\tsessionID:     \"session1\",\n\t\tclientVersion: \"SSH-2.0-beszel_0.12.0\",\n\t}\n\tctx2 := &mockSSHContext{\n\t\tsessionID:     \"session2\",\n\t\tclientVersion: \"SSH-2.0-beszel_0.11.0\",\n\t}\n\n\t// First calls should cache the versions\n\tv1 := agent.getHubVersion(\"session1\", ctx1)\n\tv2 := agent.getHubVersion(\"session2\", ctx2)\n\n\tassert.Equal(t, \"0.12.0\", v1.String())\n\tassert.Equal(t, \"0.11.0\", v2.String())\n\n\t// Verify caching by changing context but keeping same session ID\n\tctx1.clientVersion = \"SSH-2.0-beszel_0.10.0\"\n\tv1Cached := agent.getHubVersion(\"session1\", ctx1)\n\tassert.Equal(t, \"0.12.0\", v1Cached.String()) // Should still be cached version\n\n\t// New session should get new version\n\tctx3 := &mockSSHContext{\n\t\tsessionID:     \"session3\",\n\t\tclientVersion: \"SSH-2.0-beszel_0.13.0\",\n\t}\n\tv3 := agent.getHubVersion(\"session3\", ctx3)\n\tassert.Equal(t, \"0.13.0\", v3.String())\n}\n"
  },
  {
    "path": "agent/smart.go",
    "content": "//go:generate -command fetchsmartctl go run ./tools/fetchsmartctl\n//go:generate fetchsmartctl -out ./smartmontools/smartctl.exe -url https://static.beszel.dev/bin/smartctl/smartctl-nc.exe -sha 3912249c3b329249aa512ce796fd1b64d7cbd8378b68ad2756b39163d9c30b47\n\npackage agent\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n)\n\n// SmartManager manages data collection for SMART devices\ntype SmartManager struct {\n\tsync.Mutex\n\tSmartDataMap    map[string]*smart.SmartData\n\tSmartDevices    []*DeviceInfo\n\trefreshMutex    sync.Mutex\n\tlastScanTime    time.Time\n\tsmartctlPath    string\n\texcludedDevices map[string]struct{}\n}\n\ntype scanOutput struct {\n\tDevices []struct {\n\t\tName     string `json:\"name\"`\n\t\tType     string `json:\"type\"`\n\t\tInfoName string `json:\"info_name\"`\n\t\tProtocol string `json:\"protocol\"`\n\t} `json:\"devices\"`\n}\n\ntype DeviceInfo struct {\n\tName     string `json:\"name\"`\n\tType     string `json:\"type\"`\n\tInfoName string `json:\"info_name\"`\n\tProtocol string `json:\"protocol\"`\n\t// typeVerified reports whether we have already parsed SMART data for this device\n\t// with the stored parserType. When true we can skip re-running the detection logic.\n\ttypeVerified bool\n\t// parserType holds the parser type (nvme, sat, scsi) that last succeeded.\n\tparserType string\n}\n\n// deviceKey is a composite key for a device, used to identify a device uniquely.\ntype deviceKey struct {\n\tname       string\n\tdeviceType string\n}\n\nvar errNoValidSmartData = fmt.Errorf(\"no valid SMART data found\") // Error for missing data\n\n// Refresh updates SMART data for all known devices\nfunc (sm *SmartManager) Refresh(forceScan bool) error {\n\tsm.refreshMutex.Lock()\n\tdefer sm.refreshMutex.Unlock()\n\n\tscanErr := sm.ScanDevices(false)\n\tif scanErr != nil {\n\t\tslog.Debug(\"smartctl scan failed\", \"err\", scanErr)\n\t}\n\n\tdevices := sm.devicesSnapshot()\n\tvar collectErr error\n\tfor _, deviceInfo := range devices {\n\t\tif deviceInfo == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err := sm.CollectSmart(deviceInfo); err != nil {\n\t\t\tslog.Debug(\"smartctl collect failed\", \"device\", deviceInfo.Name, \"err\", err)\n\t\t\tcollectErr = err\n\t\t}\n\t}\n\n\treturn sm.resolveRefreshError(scanErr, collectErr)\n}\n\n// devicesSnapshot returns a copy of the current device slice to avoid iterating\n// while holding the primary mutex for longer than necessary.\nfunc (sm *SmartManager) devicesSnapshot() []*DeviceInfo {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tdevices := make([]*DeviceInfo, len(sm.SmartDevices))\n\tcopy(devices, sm.SmartDevices)\n\treturn devices\n}\n\n// hasSmartData reports whether any SMART data has been collected.\n// func (sm *SmartManager) hasSmartData() bool {\n// \tsm.Lock()\n// \tdefer sm.Unlock()\n\n// \treturn len(sm.SmartDataMap) > 0\n// }\n\n// resolveRefreshError determines the proper error to return after a refresh.\nfunc (sm *SmartManager) resolveRefreshError(scanErr, collectErr error) error {\n\tsm.Lock()\n\tnoDevices := len(sm.SmartDevices) == 0\n\tnoData := len(sm.SmartDataMap) == 0\n\tsm.Unlock()\n\n\tif noDevices {\n\t\tif scanErr != nil {\n\t\t\treturn scanErr\n\t\t}\n\t}\n\n\tif !noData {\n\t\treturn nil\n\t}\n\n\tif collectErr != nil {\n\t\treturn collectErr\n\t}\n\tif scanErr != nil {\n\t\treturn scanErr\n\t}\n\treturn errNoValidSmartData\n}\n\n// GetCurrentData returns the current SMART data\nfunc (sm *SmartManager) GetCurrentData() map[string]smart.SmartData {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\tresult := make(map[string]smart.SmartData, len(sm.SmartDataMap))\n\tfor key, value := range sm.SmartDataMap {\n\t\tif value != nil {\n\t\t\tresult[key] = *value\n\t\t}\n\t}\n\treturn result\n}\n\n// ScanDevices scans for SMART devices\n// Scan devices using `smartctl --scan -j`\n// If scan fails, return error\n// If scan succeeds, parse the output and update the SmartDevices slice\nfunc (sm *SmartManager) ScanDevices(force bool) error {\n\tif !force && time.Since(sm.lastScanTime) < 30*time.Minute {\n\t\treturn nil\n\t}\n\tsm.lastScanTime = time.Now()\n\tcurrentDevices := sm.devicesSnapshot()\n\n\tvar configuredDevices []*DeviceInfo\n\tif configuredRaw, ok := utils.GetEnv(\"SMART_DEVICES\"); ok {\n\t\tslog.Info(\"SMART_DEVICES\", \"value\", configuredRaw)\n\t\tconfig := strings.TrimSpace(configuredRaw)\n\t\tif config == \"\" {\n\t\t\treturn errNoValidSmartData\n\t\t}\n\n\t\tparsedDevices, err := sm.parseConfiguredDevices(config)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfiguredDevices = parsedDevices\n\t}\n\n\tvar (\n\t\tscanErr        error\n\t\tscannedDevices []*DeviceInfo\n\t\thasValidScan   bool\n\t)\n\n\tif sm.smartctlPath != \"\" {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\n\t\tcmd := exec.CommandContext(ctx, sm.smartctlPath, \"--scan\", \"-j\")\n\t\toutput, err := cmd.Output()\n\t\tif err != nil {\n\t\t\tscanErr = err\n\t\t} else {\n\t\t\tscannedDevices, hasValidScan = sm.parseScan(output)\n\t\t\tif !hasValidScan {\n\t\t\t\tscanErr = errNoValidSmartData\n\t\t\t}\n\t\t}\n\t}\n\n\t// Add eMMC devices (Linux only) by reading sysfs health fields. This does not\n\t// require smartctl and does not scan the whole device.\n\tif emmcDevices := scanEmmcDevices(); len(emmcDevices) > 0 {\n\t\tscannedDevices = append(scannedDevices, emmcDevices...)\n\t\thasValidScan = true\n\t}\n\n\t// Add Linux mdraid arrays by reading sysfs health fields. This does not\n\t// require smartctl and does not scan the whole device.\n\tif raidDevices := scanMdraidDevices(); len(raidDevices) > 0 {\n\t\tscannedDevices = append(scannedDevices, raidDevices...)\n\t\thasValidScan = true\n\t}\n\n\tfinalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)\n\tfinalDevices = sm.filterExcludedDevices(finalDevices)\n\tsm.updateSmartDevices(finalDevices)\n\n\tif len(finalDevices) == 0 {\n\t\tif scanErr != nil {\n\t\t\tslog.Debug(\"smartctl scan failed\", \"err\", scanErr)\n\t\t\treturn scanErr\n\t\t}\n\t\treturn errNoValidSmartData\n\t}\n\n\treturn nil\n}\n\nfunc (sm *SmartManager) parseConfiguredDevices(config string) ([]*DeviceInfo, error) {\n\tsplitChar, _ := utils.GetEnv(\"SMART_DEVICES_SEPARATOR\")\n\tif splitChar == \"\" {\n\t\tsplitChar = \",\"\n\t}\n\tentries := strings.Split(config, splitChar)\n\tdevices := make([]*DeviceInfo, 0, len(entries))\n\tfor _, entry := range entries {\n\t\tentry = strings.TrimSpace(entry)\n\t\tif entry == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts := strings.SplitN(entry, \":\", 2)\n\n\t\tname := strings.TrimSpace(parts[0])\n\t\tif name == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid SMART_DEVICES entry %q\", entry)\n\t\t}\n\n\t\tdevType := \"\"\n\t\tif len(parts) == 2 {\n\t\t\tdevType = strings.ToLower(strings.TrimSpace(parts[1]))\n\t\t}\n\n\t\tdevices = append(devices, &DeviceInfo{\n\t\t\tName: name,\n\t\t\tType: devType,\n\t\t})\n\t}\n\n\tif len(devices) == 0 {\n\t\treturn nil, errNoValidSmartData\n\t}\n\n\treturn devices, nil\n}\n\nfunc (sm *SmartManager) refreshExcludedDevices() {\n\trawValue, _ := utils.GetEnv(\"EXCLUDE_SMART\")\n\tsm.excludedDevices = make(map[string]struct{})\n\n\tfor entry := range strings.SplitSeq(rawValue, \",\") {\n\t\tdevice := strings.TrimSpace(entry)\n\t\tif device == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tsm.excludedDevices[device] = struct{}{}\n\t}\n}\n\nfunc (sm *SmartManager) isExcludedDevice(deviceName string) bool {\n\t_, exists := sm.excludedDevices[deviceName]\n\treturn exists\n}\n\nfunc (sm *SmartManager) filterExcludedDevices(devices []*DeviceInfo) []*DeviceInfo {\n\tif devices == nil {\n\t\treturn []*DeviceInfo{}\n\t}\n\n\texcluded := sm.excludedDevices\n\tif len(excluded) == 0 {\n\t\treturn devices\n\t}\n\n\tfiltered := make([]*DeviceInfo, 0, len(devices))\n\tfor _, device := range devices {\n\t\tif device == nil || device.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, skip := excluded[device.Name]; skip {\n\t\t\tcontinue\n\t\t}\n\t\tfiltered = append(filtered, device)\n\t}\n\treturn filtered\n}\n\n// detectSmartOutputType inspects sections that are unique to each smartctl\n// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used\n// when the reported device type is ambiguous or missing.\nfunc detectSmartOutputType(output []byte) string {\n\tvar hints struct {\n\t\tAtaSmartAttributes            json.RawMessage `json:\"ata_smart_attributes\"`\n\t\tNVMeSmartHealthInformationLog json.RawMessage `json:\"nvme_smart_health_information_log\"`\n\t\tScsiErrorCounterLog           json.RawMessage `json:\"scsi_error_counter_log\"`\n\t}\n\n\tif err := json.Unmarshal(output, &hints); err != nil {\n\t\treturn \"\"\n\t}\n\n\tswitch {\n\tcase hasJSONValue(hints.NVMeSmartHealthInformationLog):\n\t\treturn \"nvme\"\n\tcase hasJSONValue(hints.AtaSmartAttributes):\n\t\treturn \"sat\"\n\tcase hasJSONValue(hints.ScsiErrorCounterLog):\n\t\treturn \"scsi\"\n\tdefault:\n\t\treturn \"sat\"\n\t}\n}\n\n// hasJSONValue reports whether a JSON payload contains a concrete value. The\n// smartctl output often emits \"null\" for sections that do not apply, so we\n// only treat non-null content as a hint.\nfunc hasJSONValue(raw json.RawMessage) bool {\n\tif len(raw) == 0 {\n\t\treturn false\n\t}\n\ttrimmed := strings.TrimSpace(string(raw))\n\treturn trimmed != \"\" && trimmed != \"null\"\n}\n\nfunc normalizeParserType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"nvme\", \"sntasmedia\", \"sntrealtek\":\n\t\treturn \"nvme\"\n\tcase \"sat\", \"ata\":\n\t\treturn \"sat\"\n\tcase \"scsi\":\n\t\treturn \"scsi\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\n// makeDeviceKey creates a composite key from device name and type.\n// This allows multiple drives under the same device path (e.g., RAID controllers)\n// to be tracked separately.\nfunc makeDeviceKey(name, deviceType string) deviceKey {\n\treturn deviceKey{name: name, deviceType: deviceType}\n}\n\n// parseSmartOutput attempts each SMART parser, optionally detecting the type when\n// it is not provided, and updates the device info when a parser succeeds.\nfunc (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte) bool {\n\tparsers := []struct {\n\t\tType  string\n\t\tParse func([]byte) (bool, int)\n\t}{\n\t\t{Type: \"nvme\", Parse: sm.parseSmartForNvme},\n\t\t{Type: \"sat\", Parse: sm.parseSmartForSata},\n\t\t{Type: \"scsi\", Parse: sm.parseSmartForScsi},\n\t}\n\n\tdeviceType := normalizeParserType(deviceInfo.parserType)\n\tif deviceType == \"\" {\n\t\tdeviceType = normalizeParserType(deviceInfo.Type)\n\t}\n\tif deviceInfo.parserType == \"\" {\n\t\tswitch deviceType {\n\t\tcase \"nvme\", \"sat\", \"scsi\":\n\t\t\tdeviceInfo.parserType = deviceType\n\t\t}\n\t}\n\n\t// Only run the type detection when we do not yet know which parser works\n\t// or the previous attempt failed.\n\tneedsDetection := deviceType == \"\" || !deviceInfo.typeVerified\n\tif needsDetection {\n\t\tstructureType := detectSmartOutputType(output)\n\t\tif deviceType != structureType {\n\t\t\tdeviceType = structureType\n\t\t\tdeviceInfo.parserType = structureType\n\t\t\tdeviceInfo.typeVerified = false\n\t\t}\n\t\tif deviceInfo.Type == \"\" || strings.EqualFold(deviceInfo.Type, structureType) {\n\t\t\tdeviceInfo.Type = structureType\n\t\t}\n\t}\n\n\t// Try the most likely parser first, but keep the remaining parsers in reserve\n\t// so an incorrect hint never leaves the device unparsed.\n\tselectedParsers := make([]struct {\n\t\tType  string\n\t\tParse func([]byte) (bool, int)\n\t}, 0, len(parsers))\n\tif deviceType != \"\" {\n\t\tfor _, parser := range parsers {\n\t\t\tif parser.Type == deviceType {\n\t\t\t\tselectedParsers = append(selectedParsers, parser)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tfor _, parser := range parsers {\n\t\talreadySelected := false\n\t\tfor _, selected := range selectedParsers {\n\t\t\tif selected.Type == parser.Type {\n\t\t\t\talreadySelected = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif alreadySelected {\n\t\t\tcontinue\n\t\t}\n\t\tselectedParsers = append(selectedParsers, parser)\n\t}\n\n\t// Try the selected parsers in order until we find one that succeeds.\n\tfor _, parser := range selectedParsers {\n\t\thasData, _ := parser.Parse(output)\n\t\tif hasData {\n\t\t\tdeviceInfo.parserType = parser.Type\n\t\t\tif deviceInfo.Type == \"\" || strings.EqualFold(deviceInfo.Type, parser.Type) {\n\t\t\t\tdeviceInfo.Type = parser.Type\n\t\t\t}\n\t\t\t// Remember that this parser is valid so future refreshes can bypass\n\t\t\t// detection entirely.\n\t\t\tdeviceInfo.typeVerified = true\n\t\t\treturn true\n\t\t}\n\t\tslog.Debug(\"parser failed\", \"device\", deviceInfo.Name, \"parser\", parser.Type)\n\t}\n\n\t// Leave verification false so the next pass will attempt detection again.\n\tdeviceInfo.typeVerified = false\n\tslog.Debug(\"parsing failed\", \"device\", deviceInfo.Name)\n\treturn false\n}\n\n// CollectSmart collects SMART data for a device\n// Collect data using `smartctl -d <type> -aj /dev/<device>` when device type is known\n// Always attempts to parse output even if command fails, as some data may still be available\n// If collect fails, return error\n// If collect succeeds, parse the output and update the SmartDataMap\n// Uses -n standby to avoid waking up sleeping disks, but bypasses standby mode\n// for initial data collection when no cached data exists\nfunc (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {\n\tif deviceInfo != nil && sm.isExcludedDevice(deviceInfo.Name) {\n\t\treturn errNoValidSmartData\n\t}\n\n\t// mdraid health is not exposed via SMART; Linux exposes array state in sysfs.\n\tif deviceInfo != nil {\n\t\tif ok, err := sm.collectMdraidHealth(deviceInfo); ok {\n\t\t\treturn err\n\t\t}\n\t}\n\t// eMMC health is not exposed via SMART on Linux, but the kernel provides\n\t// wear / EOL indicators via sysfs. Prefer that path when available.\n\tif deviceInfo != nil {\n\t\tif ok, err := sm.collectEmmcHealth(deviceInfo); ok {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif sm.smartctlPath == \"\" {\n\t\treturn errNoValidSmartData\n\t}\n\n\t// slog.Info(\"collecting SMART data\", \"device\", deviceInfo.Name, \"type\", deviceInfo.Type, \"has_existing_data\", sm.hasDataForDevice(deviceInfo.Name))\n\n\t// Check if we have any existing data for this device\n\thasExistingData := sm.hasDataForDevice(deviceInfo.Name)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\t// Try with -n standby first if we have existing data\n\targs := sm.smartctlArgs(deviceInfo, hasExistingData)\n\tcmd := exec.CommandContext(ctx, sm.smartctlPath, args...)\n\toutput, err := cmd.CombinedOutput()\n\n\t// Check if device is in standby (exit status 2)\n\tif exitErr, ok := errors.AsType[*exec.ExitError](err); ok && exitErr.ExitCode() == 2 {\n\t\tif hasExistingData {\n\t\t\t// Device is in standby and we have cached data, keep using cache\n\t\t\treturn nil\n\t\t}\n\t\t// No cached data, need to collect initial data by bypassing standby\n\t\tctx2, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)\n\t\tdefer cancel2()\n\t\targs = sm.smartctlArgs(deviceInfo, false)\n\t\tcmd = exec.CommandContext(ctx2, sm.smartctlPath, args...)\n\t\toutput, err = cmd.CombinedOutput()\n\t}\n\n\thasValidData := sm.parseSmartOutput(deviceInfo, output)\n\n\t// If NVMe controller path failed, try namespace path as fallback.\n\t// NVMe controllers (/dev/nvme0) don't always support SMART queries. See github.com/henrygd/beszel/issues/1504\n\tif !hasValidData && err != nil && isNvmeControllerPath(deviceInfo.Name) {\n\t\tcontrollerPath := deviceInfo.Name\n\t\tnamespacePath := controllerPath + \"n1\"\n\t\tif !sm.isExcludedDevice(namespacePath) {\n\t\t\tdeviceInfo.Name = namespacePath\n\n\t\t\tctx3, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)\n\t\t\tdefer cancel3()\n\t\t\targs = sm.smartctlArgs(deviceInfo, false)\n\t\t\tcmd = exec.CommandContext(ctx3, sm.smartctlPath, args...)\n\t\t\toutput, err = cmd.CombinedOutput()\n\t\t\thasValidData = sm.parseSmartOutput(deviceInfo, output)\n\n\t\t\t// Auto-exclude the controller path so future scans don't re-add it\n\t\t\tif hasValidData {\n\t\t\t\tsm.Lock()\n\t\t\t\tif sm.excludedDevices == nil {\n\t\t\t\t\tsm.excludedDevices = make(map[string]struct{})\n\t\t\t\t}\n\t\t\t\tsm.excludedDevices[controllerPath] = struct{}{}\n\t\t\t\tsm.Unlock()\n\t\t\t\tslog.Debug(\"auto-excluded NVMe controller path\", \"path\", controllerPath)\n\t\t\t}\n\t\t}\n\t}\n\n\tif !hasValidData {\n\t\tif err != nil {\n\t\t\tslog.Info(\"smartctl failed\", \"device\", deviceInfo.Name, \"err\", err)\n\t\t\treturn err\n\t\t}\n\t\tslog.Info(\"no valid SMART data found\", \"device\", deviceInfo.Name)\n\t\treturn errNoValidSmartData\n\t}\n\n\treturn nil\n}\n\n// smartctlArgs returns the arguments for the smartctl command\n// based on the device type and whether to include standby mode\nfunc (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string {\n\targs := make([]string, 0, 9)\n\tvar deviceType, parserType string\n\n\tif deviceInfo != nil {\n\t\tdeviceType = strings.ToLower(deviceInfo.Type)\n\t\tparserType = strings.ToLower(deviceInfo.parserType)\n\t\t// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345\n\t\tif deviceType != \"\" && deviceType != \"scsi\" && deviceType != \"ata\" {\n\t\t\targs = append(args, \"-d\", deviceInfo.Type)\n\t\t}\n\t}\n\n\targs = append(args, \"-a\", \"--json=c\")\n\teffectiveType := parserType\n\tif effectiveType == \"\" {\n\t\teffectiveType = deviceType\n\t}\n\tif effectiveType == \"sat\" || effectiveType == \"ata\" {\n\t\targs = append(args, \"-l\", \"devstat\")\n\t}\n\n\tif includeStandby {\n\t\targs = append(args, \"-n\", \"standby\")\n\t}\n\n\tif deviceInfo != nil {\n\t\targs = append(args, deviceInfo.Name)\n\t}\n\n\treturn args\n}\n\n// hasDataForDevice checks if we have cached SMART data for a specific device\nfunc (sm *SmartManager) hasDataForDevice(deviceName string) bool {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\t// Check if any cached data has this device name\n\tfor _, data := range sm.SmartDataMap {\n\t\tif data != nil && data.DiskName == deviceName {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// parseScan parses the output of smartctl --scan -j and returns the discovered devices.\nfunc (sm *SmartManager) parseScan(output []byte) ([]*DeviceInfo, bool) {\n\tscan := &scanOutput{}\n\n\tif err := json.Unmarshal(output, scan); err != nil {\n\t\treturn nil, false\n\t}\n\n\tif len(scan.Devices) == 0 {\n\t\tslog.Debug(\"no devices found in smartctl scan\")\n\t\treturn nil, false\n\t}\n\n\tdevices := make([]*DeviceInfo, 0, len(scan.Devices))\n\tfor _, device := range scan.Devices {\n\t\tslog.Debug(\"smartctl scan\", \"name\", device.Name, \"type\", device.Type, \"protocol\", device.Protocol)\n\t\tdevices = append(devices, &DeviceInfo{\n\t\t\tName:     device.Name,\n\t\t\tType:     device.Type,\n\t\t\tInfoName: device.InfoName,\n\t\t\tProtocol: device.Protocol,\n\t\t})\n\t}\n\n\treturn devices, true\n}\n\n// mergeDeviceLists combines scanned and configured SMART devices, preferring\n// configured SMART_DEVICES when both sources reference the same device.\nfunc mergeDeviceLists(existing, scanned, configured []*DeviceInfo) []*DeviceInfo {\n\tif len(scanned) == 0 && len(configured) == 0 {\n\t\treturn existing\n\t}\n\n\t// buildUniqueNameIndex returns devices that appear exactly once by name.\n\t// It is used to safely apply name-only fallbacks without RAID ambiguity.\n\tbuildUniqueNameIndex := func(devices []*DeviceInfo) map[string]*DeviceInfo {\n\t\tcounts := make(map[string]int, len(devices))\n\t\tfor _, dev := range devices {\n\t\t\tif dev == nil || dev.Name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcounts[dev.Name]++\n\t\t}\n\t\tunique := make(map[string]*DeviceInfo, len(counts))\n\t\tfor _, dev := range devices {\n\t\t\tif dev == nil || dev.Name == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif counts[dev.Name] == 1 {\n\t\t\t\tunique[dev.Name] = dev\n\t\t\t}\n\t\t}\n\t\treturn unique\n\t}\n\n\t// preserveVerifiedType copies the verified type/parser metadata from an existing\n\t// device record so that subsequent scans/config updates never downgrade a\n\t// previously verified device.\n\tpreserveVerifiedType := func(target, prev *DeviceInfo) {\n\t\tif prev == nil || !prev.typeVerified {\n\t\t\treturn\n\t\t}\n\t\ttarget.Type = prev.Type\n\t\ttarget.typeVerified = true\n\t\ttarget.parserType = prev.parserType\n\t}\n\n\t// applyConfiguredMetadata updates a matched device with any configured\n\t// overrides, preserving verified type data when present.\n\tapplyConfiguredMetadata := func(existingDev, configuredDev *DeviceInfo) {\n\t\t// Only update the type if it has not been verified yet; otherwise we\n\t\t// keep the existing verified metadata intact.\n\t\tif configuredDev.Type != \"\" && !existingDev.typeVerified {\n\t\t\tnewType := strings.TrimSpace(configuredDev.Type)\n\t\t\texistingDev.Type = newType\n\t\t\texistingDev.typeVerified = false\n\t\t\texistingDev.parserType = normalizeParserType(newType)\n\t\t}\n\t\tif configuredDev.InfoName != \"\" {\n\t\t\texistingDev.InfoName = configuredDev.InfoName\n\t\t}\n\t\tif configuredDev.Protocol != \"\" {\n\t\t\texistingDev.Protocol = configuredDev.Protocol\n\t\t}\n\t}\n\n\texistingIndex := make(map[deviceKey]*DeviceInfo, len(existing))\n\tfor _, dev := range existing {\n\t\tif dev == nil || dev.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\texistingIndex[makeDeviceKey(dev.Name, dev.Type)] = dev\n\t}\n\texistingByName := buildUniqueNameIndex(existing)\n\n\tfinalDevices := make([]*DeviceInfo, 0, len(scanned)+len(configured))\n\tdeviceIndex := make(map[deviceKey]*DeviceInfo, len(scanned)+len(configured))\n\n\t// Start with the newly scanned devices so we always surface fresh metadata,\n\t// but ensure we retain any previously verified parser assignment.\n\tfor _, scannedDevice := range scanned {\n\t\tif scannedDevice == nil || scannedDevice.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Work on a copy so we can safely adjust metadata without mutating the\n\t\t// input slices that may be reused elsewhere.\n\t\tcopyDev := *scannedDevice\n\t\tkey := makeDeviceKey(copyDev.Name, copyDev.Type)\n\t\tif prev := existingIndex[key]; prev != nil {\n\t\t\tpreserveVerifiedType(&copyDev, prev)\n\t\t} else if prev := existingByName[copyDev.Name]; prev != nil {\n\t\t\tpreserveVerifiedType(&copyDev, prev)\n\t\t}\n\n\t\tfinalDevices = append(finalDevices, &copyDev)\n\t\tcopyKey := makeDeviceKey(copyDev.Name, copyDev.Type)\n\t\tdeviceIndex[copyKey] = finalDevices[len(finalDevices)-1]\n\t}\n\tdeviceIndexByName := buildUniqueNameIndex(finalDevices)\n\n\t// Merge configured devices on top so users can override scan results (except\n\t// for verified type information).\n\tfor _, configuredDevice := range configured {\n\t\tif configuredDevice == nil || configuredDevice.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tkey := makeDeviceKey(configuredDevice.Name, configuredDevice.Type)\n\t\tif existingDev, ok := deviceIndex[key]; ok {\n\t\t\tapplyConfiguredMetadata(existingDev, configuredDevice)\n\t\t\tcontinue\n\t\t}\n\t\tif existingDev := deviceIndexByName[configuredDevice.Name]; existingDev != nil {\n\t\t\tapplyConfiguredMetadata(existingDev, configuredDevice)\n\t\t\tcontinue\n\t\t}\n\n\t\tcopyDev := *configuredDevice\n\t\tkey = makeDeviceKey(copyDev.Name, copyDev.Type)\n\t\tif prev := existingIndex[key]; prev != nil {\n\t\t\tpreserveVerifiedType(&copyDev, prev)\n\t\t} else if prev := existingByName[copyDev.Name]; prev != nil {\n\t\t\tpreserveVerifiedType(&copyDev, prev)\n\t\t} else if copyDev.Type != \"\" {\n\t\t\tcopyDev.parserType = normalizeParserType(copyDev.Type)\n\t\t}\n\n\t\tfinalDevices = append(finalDevices, &copyDev)\n\t\tcopyKey := makeDeviceKey(copyDev.Name, copyDev.Type)\n\t\tdeviceIndex[copyKey] = finalDevices[len(finalDevices)-1]\n\t}\n\n\treturn finalDevices\n}\n\n// updateSmartDevices replaces the cached device list and prunes SMART data\n// entries whose backing device no longer exists.\nfunc (sm *SmartManager) updateSmartDevices(devices []*DeviceInfo) {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tsm.SmartDevices = devices\n\n\tif len(sm.SmartDataMap) == 0 {\n\t\treturn\n\t}\n\n\tvalidKeys := make(map[deviceKey]struct{}, len(devices))\n\tnameCounts := make(map[string]int, len(devices))\n\tfor _, device := range devices {\n\t\tif device == nil || device.Name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tvalidKeys[makeDeviceKey(device.Name, device.Type)] = struct{}{}\n\t\tnameCounts[device.Name]++\n\t}\n\n\tfor key, data := range sm.SmartDataMap {\n\t\tif data == nil {\n\t\t\tdelete(sm.SmartDataMap, key)\n\t\t\tcontinue\n\t\t}\n\n\t\tif data.DiskType == \"\" {\n\t\t\tif nameCounts[data.DiskName] == 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if _, ok := validKeys[makeDeviceKey(data.DiskName, data.DiskType)]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tdelete(sm.SmartDataMap, key)\n\t}\n}\n\n// isVirtualDevice checks if a device is a virtual disk that should be filtered out\nfunc (sm *SmartManager) isVirtualDevice(data *smart.SmartInfoForSata) bool {\n\tvendorUpper := strings.ToUpper(data.ScsiVendor)\n\tproductUpper := strings.ToUpper(data.ScsiProduct)\n\tmodelUpper := strings.ToUpper(data.ModelName)\n\n\treturn sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper)\n}\n\n// isVirtualDeviceNvme checks if an NVMe device is a virtual disk that should be filtered out\nfunc (sm *SmartManager) isVirtualDeviceNvme(data *smart.SmartInfoForNvme) bool {\n\tmodelUpper := strings.ToUpper(data.ModelName)\n\n\treturn sm.isVirtualDeviceFromStrings(modelUpper)\n}\n\n// isVirtualDeviceScsi checks if a SCSI device is a virtual disk that should be filtered out\nfunc (sm *SmartManager) isVirtualDeviceScsi(data *smart.SmartInfoForScsi) bool {\n\tvendorUpper := strings.ToUpper(data.ScsiVendor)\n\tproductUpper := strings.ToUpper(data.ScsiProduct)\n\tmodelUpper := strings.ToUpper(data.ScsiModelName)\n\n\treturn sm.isVirtualDeviceFromStrings(vendorUpper, productUpper, modelUpper)\n}\n\n// isVirtualDeviceFromStrings checks if any of the provided strings indicate a virtual device\nfunc (sm *SmartManager) isVirtualDeviceFromStrings(fields ...string) bool {\n\tfor _, field := range fields {\n\t\tfieldUpper := strings.ToUpper(field)\n\t\tswitch {\n\t\tcase strings.Contains(fieldUpper, \"IET\"), // iSCSI Enterprise Target\n\t\t\tstrings.Contains(fieldUpper, \"VIRTUAL\"),\n\t\t\tstrings.Contains(fieldUpper, \"QEMU\"),\n\t\t\tstrings.Contains(fieldUpper, \"VBOX\"),\n\t\t\tstrings.Contains(fieldUpper, \"VMWARE\"),\n\t\t\tstrings.Contains(fieldUpper, \"MSFT\"): // Microsoft Hyper-V\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// parseSmartForSata parses the output of smartctl --all -j for SATA/ATA devices and updates the SmartDataMap\n// Returns hasValidData and exitStatus\nfunc (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {\n\tvar data smart.SmartInfoForSata\n\n\tif err := json.Unmarshal(output, &data); err != nil {\n\t\treturn false, 0\n\t}\n\n\tif data.SerialNumber == \"\" {\n\t\tslog.Debug(\"no serial number\", \"device\", data.Device.Name)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\t// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)\n\tif sm.isVirtualDevice(&data) {\n\t\tslog.Debug(\"skipping smart\", \"device\", data.Device.Name, \"model\", data.ModelName)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tkeyName := data.SerialNumber\n\n\t// if device does not exist in SmartDataMap, initialize it\n\tif _, ok := sm.SmartDataMap[keyName]; !ok {\n\t\tsm.SmartDataMap[keyName] = &smart.SmartData{}\n\t}\n\n\t// update SmartData\n\tsmartData := sm.SmartDataMap[keyName]\n\t// smartData.ModelFamily = data.ModelFamily\n\tsmartData.ModelName = data.ModelName\n\tsmartData.SerialNumber = data.SerialNumber\n\tsmartData.FirmwareVersion = data.FirmwareVersion\n\tsmartData.Capacity = data.UserCapacity.Bytes\n\tsmartData.Temperature = data.Temperature.Current\n\tsmartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)\n\tsmartData.DiskName = data.Device.Name\n\tsmartData.DiskType = data.Device.Type\n\n\t// get values from ata_device_statistics if necessary\n\tvar ataDeviceStats smart.AtaDeviceStatistics\n\tif smartData.Temperature == 0 {\n\t\tif temp := findAtaDeviceStatisticsValue(&data, &ataDeviceStats, 5, \"Current Temperature\", 0, 255); temp != nil {\n\t\t\tsmartData.Temperature = uint8(*temp)\n\t\t}\n\t}\n\n\t// update SmartAttributes\n\tsmartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table))\n\tfor _, attr := range data.AtaSmartAttributes.Table {\n\t\trawValue := uint64(attr.Raw.Value)\n\t\tif parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {\n\t\t\trawValue = parsed\n\t\t}\n\t\tsmartAttr := &smart.SmartAttribute{\n\t\t\tID:         attr.ID,\n\t\t\tName:       attr.Name,\n\t\t\tValue:      attr.Value,\n\t\t\tWorst:      attr.Worst,\n\t\t\tThreshold:  attr.Thresh,\n\t\t\tRawValue:   rawValue,\n\t\t\tRawString:  attr.Raw.String,\n\t\t\tWhenFailed: attr.WhenFailed,\n\t\t}\n\t\tsmartData.Attributes = append(smartData.Attributes, smartAttr)\n\t}\n\tsm.SmartDataMap[keyName] = smartData\n\n\treturn true, data.Smartctl.ExitStatus\n}\n\nfunc getSmartStatus(temperature uint8, passed bool) string {\n\tif passed {\n\t\treturn \"PASSED\"\n\t} else if temperature > 0 {\n\t\treturn \"FAILED\"\n\t} else {\n\t\treturn \"UNKNOWN\"\n\t}\n}\n\n// findAtaDeviceStatisticsEntry centralizes ATA devstat lookups so additional\n// metrics can be pulled from the same structure in the future.\nfunc findAtaDeviceStatisticsValue(data *smart.SmartInfoForSata, ataDeviceStats *smart.AtaDeviceStatistics, entryNumber uint8, entryName string, minValue, maxValue int64) *int64 {\n\tif len(ataDeviceStats.Pages) == 0 {\n\t\tif len(data.AtaDeviceStatistics) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif err := json.Unmarshal(data.AtaDeviceStatistics, ataDeviceStats); err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\tfor pageIdx := range ataDeviceStats.Pages {\n\t\tpage := &ataDeviceStats.Pages[pageIdx]\n\t\tif page.Number != entryNumber {\n\t\t\tcontinue\n\t\t}\n\t\tfor entryIdx := range page.Table {\n\t\t\tentry := &page.Table[entryIdx]\n\t\t\tif !strings.EqualFold(entry.Name, entryName) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif entry.Value == nil || *entry.Value < minValue || *entry.Value > maxValue {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn entry.Value\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {\n\tvar data smart.SmartInfoForScsi\n\n\tif err := json.Unmarshal(output, &data); err != nil {\n\t\treturn false, 0\n\t}\n\n\tif data.SerialNumber == \"\" {\n\t\tslog.Debug(\"no serial number\", \"device\", data.Device.Name)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\t// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)\n\tif sm.isVirtualDeviceScsi(&data) {\n\t\tslog.Debug(\"skipping smart\", \"device\", data.Device.Name, \"model\", data.ScsiModelName)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tkeyName := data.SerialNumber\n\tif _, ok := sm.SmartDataMap[keyName]; !ok {\n\t\tsm.SmartDataMap[keyName] = &smart.SmartData{}\n\t}\n\n\tsmartData := sm.SmartDataMap[keyName]\n\tsmartData.ModelName = data.ScsiModelName\n\tsmartData.SerialNumber = data.SerialNumber\n\tsmartData.FirmwareVersion = data.ScsiRevision\n\tsmartData.Capacity = data.UserCapacity.Bytes\n\tsmartData.Temperature = data.Temperature.Current\n\tsmartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)\n\tsmartData.DiskName = data.Device.Name\n\tsmartData.DiskType = data.Device.Type\n\n\tattributes := make([]*smart.SmartAttribute, 0, 10)\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"PowerOnHours\", RawValue: data.PowerOnTime.Hours})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"PowerOnMinutes\", RawValue: data.PowerOnTime.Minutes})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"GrownDefectList\", RawValue: data.ScsiGrownDefectList})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"StartStopCycles\", RawValue: data.ScsiStartStopCycleCounter.AccumulatedStartStopCycles})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"LoadUnloadCycles\", RawValue: data.ScsiStartStopCycleCounter.AccumulatedLoadUnloadCycles})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"StartStopSpecified\", RawValue: data.ScsiStartStopCycleCounter.SpecifiedCycleCountOverDeviceLifetime})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"LoadUnloadSpecified\", RawValue: data.ScsiStartStopCycleCounter.SpecifiedLoadUnloadCountOverDeviceLifetime})\n\n\treadStats := data.ScsiErrorCounterLog.Read\n\twriteStats := data.ScsiErrorCounterLog.Write\n\tverifyStats := data.ScsiErrorCounterLog.Verify\n\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"ReadTotalErrorsCorrected\", RawValue: readStats.TotalErrorsCorrected})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"ReadTotalUncorrectedErrors\", RawValue: readStats.TotalUncorrectedErrors})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"ReadCorrectionAlgorithmInvocations\", RawValue: readStats.CorrectionAlgorithmInvocations})\n\tif val := parseScsiGigabytesProcessed(readStats.GigabytesProcessed); val >= 0 {\n\t\tattributes = append(attributes, &smart.SmartAttribute{Name: \"ReadGigabytesProcessed\", RawValue: uint64(val)})\n\t}\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"WriteTotalErrorsCorrected\", RawValue: writeStats.TotalErrorsCorrected})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"WriteTotalUncorrectedErrors\", RawValue: writeStats.TotalUncorrectedErrors})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"WriteCorrectionAlgorithmInvocations\", RawValue: writeStats.CorrectionAlgorithmInvocations})\n\tif val := parseScsiGigabytesProcessed(writeStats.GigabytesProcessed); val >= 0 {\n\t\tattributes = append(attributes, &smart.SmartAttribute{Name: \"WriteGigabytesProcessed\", RawValue: uint64(val)})\n\t}\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"VerifyTotalErrorsCorrected\", RawValue: verifyStats.TotalErrorsCorrected})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"VerifyTotalUncorrectedErrors\", RawValue: verifyStats.TotalUncorrectedErrors})\n\tattributes = append(attributes, &smart.SmartAttribute{Name: \"VerifyCorrectionAlgorithmInvocations\", RawValue: verifyStats.CorrectionAlgorithmInvocations})\n\tif val := parseScsiGigabytesProcessed(verifyStats.GigabytesProcessed); val >= 0 {\n\t\tattributes = append(attributes, &smart.SmartAttribute{Name: \"VerifyGigabytesProcessed\", RawValue: uint64(val)})\n\t}\n\n\tsmartData.Attributes = attributes\n\tsm.SmartDataMap[keyName] = smartData\n\n\treturn true, data.Smartctl.ExitStatus\n}\n\nfunc parseScsiGigabytesProcessed(value string) int64 {\n\tif value == \"\" {\n\t\treturn -1\n\t}\n\tnormalized := strings.ReplaceAll(value, \",\", \"\")\n\tparsed, err := strconv.ParseInt(normalized, 10, 64)\n\tif err != nil {\n\t\treturn -1\n\t}\n\treturn parsed\n}\n\n// parseSmartForNvme parses the output of smartctl --all -j /dev/nvmeX and updates the SmartDataMap\n// Returns hasValidData and exitStatus\nfunc (sm *SmartManager) parseSmartForNvme(output []byte) (bool, int) {\n\tdata := &smart.SmartInfoForNvme{}\n\n\tif err := json.Unmarshal(output, &data); err != nil {\n\t\treturn false, 0\n\t}\n\n\tif data.SerialNumber == \"\" {\n\t\tslog.Debug(\"no serial number\", \"device\", data.Device.Name)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\t// Skip virtual devices (e.g., Kubernetes PVCs, QEMU, VirtualBox, etc.)\n\tif sm.isVirtualDeviceNvme(data) {\n\t\tslog.Debug(\"skipping smart\", \"device\", data.Device.Name, \"model\", data.ModelName)\n\t\treturn false, data.Smartctl.ExitStatus\n\t}\n\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tkeyName := data.SerialNumber\n\n\t// if device does not exist in SmartDataMap, initialize it\n\tif _, ok := sm.SmartDataMap[keyName]; !ok {\n\t\tsm.SmartDataMap[keyName] = &smart.SmartData{}\n\t}\n\n\t// update SmartData\n\tsmartData := sm.SmartDataMap[keyName]\n\tsmartData.ModelName = data.ModelName\n\tsmartData.SerialNumber = data.SerialNumber\n\tsmartData.FirmwareVersion = data.FirmwareVersion\n\tsmartData.Capacity = data.UserCapacity.Bytes\n\tsmartData.Temperature = data.NVMeSmartHealthInformationLog.Temperature\n\tsmartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)\n\tsmartData.DiskName = data.Device.Name\n\tsmartData.DiskType = data.Device.Type\n\n\t// nvme attributes does not follow the same format as ata attributes,\n\t// so we manually map each field to SmartAttributes\n\tlog := data.NVMeSmartHealthInformationLog\n\tsmartData.Attributes = []*smart.SmartAttribute{\n\t\t{Name: \"CriticalWarning\", RawValue: uint64(log.CriticalWarning)},\n\t\t{Name: \"Temperature\", RawValue: uint64(log.Temperature)},\n\t\t{Name: \"AvailableSpare\", RawValue: uint64(log.AvailableSpare)},\n\t\t{Name: \"AvailableSpareThreshold\", RawValue: uint64(log.AvailableSpareThreshold)},\n\t\t{Name: \"PercentageUsed\", RawValue: uint64(log.PercentageUsed)},\n\t\t{Name: \"DataUnitsRead\", RawValue: log.DataUnitsRead},\n\t\t{Name: \"DataUnitsWritten\", RawValue: log.DataUnitsWritten},\n\t\t{Name: \"HostReads\", RawValue: uint64(log.HostReads)},\n\t\t{Name: \"HostWrites\", RawValue: uint64(log.HostWrites)},\n\t\t{Name: \"ControllerBusyTime\", RawValue: uint64(log.ControllerBusyTime)},\n\t\t{Name: \"PowerCycles\", RawValue: uint64(log.PowerCycles)},\n\t\t{Name: \"PowerOnHours\", RawValue: uint64(log.PowerOnHours)},\n\t\t{Name: \"UnsafeShutdowns\", RawValue: uint64(log.UnsafeShutdowns)},\n\t\t{Name: \"MediaErrors\", RawValue: uint64(log.MediaErrors)},\n\t\t{Name: \"NumErrLogEntries\", RawValue: uint64(log.NumErrLogEntries)},\n\t\t{Name: \"WarningTempTime\", RawValue: uint64(log.WarningTempTime)},\n\t\t{Name: \"CriticalCompTime\", RawValue: uint64(log.CriticalCompTime)},\n\t}\n\n\tsm.SmartDataMap[keyName] = smartData\n\n\treturn true, data.Smartctl.ExitStatus\n}\n\n// detectSmartctl checks if smartctl is installed, returns an error if not\nfunc (sm *SmartManager) detectSmartctl() (string, error) {\n\tisWindows := runtime.GOOS == \"windows\"\n\n\t// Load embedded smartctl.exe for Windows amd64 builds.\n\tif isWindows && runtime.GOARCH == \"amd64\" {\n\t\tif path, err := ensureEmbeddedSmartctl(); err == nil {\n\t\t\treturn path, nil\n\t\t}\n\t}\n\n\tif path, err := exec.LookPath(\"smartctl\"); err == nil {\n\t\treturn path, nil\n\t}\n\tlocations := []string{}\n\tif isWindows {\n\t\tlocations = append(locations,\n\t\t\t\"C:\\\\Program Files\\\\smartmontools\\\\bin\\\\smartctl.exe\",\n\t\t)\n\t} else {\n\t\tlocations = append(locations, \"/opt/homebrew/bin/smartctl\")\n\t}\n\tfor _, location := range locations {\n\t\tif _, err := os.Stat(location); err == nil {\n\t\t\treturn location, nil\n\t\t}\n\t}\n\treturn \"\", errors.New(\"smartctl not found\")\n}\n\n// isNvmeControllerPath checks if the path matches an NVMe controller pattern\n// like /dev/nvme0, /dev/nvme1, etc. (without namespace suffix like n1)\nfunc isNvmeControllerPath(path string) bool {\n\tbase := filepath.Base(path)\n\tif !strings.HasPrefix(base, \"nvme\") {\n\t\treturn false\n\t}\n\tsuffix := strings.TrimPrefix(base, \"nvme\")\n\tif suffix == \"\" {\n\t\treturn false\n\t}\n\t// Controller paths are just \"nvme\" + digits (e.g., nvme0, nvme1)\n\t// Namespace paths have \"n\" after the controller number (e.g., nvme0n1)\n\tfor _, c := range suffix {\n\t\tif c < '0' || c > '9' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// NewSmartManager creates and initializes a new SmartManager\nfunc NewSmartManager() (*SmartManager, error) {\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\tsm.refreshExcludedDevices()\n\tpath, err := sm.detectSmartctl()\n\tslog.Debug(\"smartctl\", \"path\", path, \"err\", err)\n\tif err != nil {\n\t\t// Keep the previous fail-fast behavior unless this Linux host exposes\n\t\t// eMMC or mdraid health via sysfs, in which case smartctl is optional.\n\t\tif runtime.GOOS == \"linux\" {\n\t\t\tif len(scanEmmcDevices()) > 0 || len(scanMdraidDevices()) > 0 {\n\t\t\t\treturn sm, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\tsm.smartctlPath = path\n\treturn sm, nil\n}\n"
  },
  {
    "path": "agent/smart_nonwindows.go",
    "content": "//go:build !windows\n\npackage agent\n\nimport \"errors\"\n\nfunc ensureEmbeddedSmartctl() (string, error) {\n\treturn \"\", errors.ErrUnsupported\n}\n"
  },
  {
    "path": "agent/smart_test.go",
    "content": "//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/internal/entities/smart\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseSmartForScsi(t *testing.T) {\n\tfixturePath := filepath.Join(\"test-data\", \"smart\", \"scsi.json\")\n\tdata, err := os.ReadFile(fixturePath)\n\tif err != nil {\n\t\tt.Fatalf(\"failed reading fixture: %v\", err)\n\t}\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\thasData, exitStatus := sm.parseSmartForScsi(data)\n\tif !hasData {\n\t\tt.Fatalf(\"expected SCSI data to parse successfully\")\n\t}\n\tif exitStatus != 0 {\n\t\tt.Fatalf(\"expected exit status 0, got %d\", exitStatus)\n\t}\n\n\tdeviceData, ok := sm.SmartDataMap[\"9YHSDH9B\"]\n\tif !ok {\n\t\tt.Fatalf(\"expected smart data entry for serial 9YHSDH9B\")\n\t}\n\n\tassert.Equal(t, deviceData.ModelName, \"YADRO WUH721414AL4204\")\n\tassert.Equal(t, deviceData.SerialNumber, \"9YHSDH9B\")\n\tassert.Equal(t, deviceData.FirmwareVersion, \"C240\")\n\tassert.Equal(t, deviceData.DiskName, \"/dev/sde\")\n\tassert.Equal(t, deviceData.DiskType, \"scsi\")\n\tassert.EqualValues(t, deviceData.Temperature, 34)\n\tassert.Equal(t, deviceData.SmartStatus, \"PASSED\")\n\tassert.EqualValues(t, deviceData.Capacity, 14000519643136)\n\n\tif len(deviceData.Attributes) == 0 {\n\t\tt.Fatalf(\"expected attributes to be populated\")\n\t}\n\n\tassertAttrValue(t, deviceData.Attributes, \"PowerOnHours\", 458)\n\tassertAttrValue(t, deviceData.Attributes, \"PowerOnMinutes\", 25)\n\tassertAttrValue(t, deviceData.Attributes, \"GrownDefectList\", 0)\n\tassertAttrValue(t, deviceData.Attributes, \"StartStopCycles\", 2)\n\tassertAttrValue(t, deviceData.Attributes, \"LoadUnloadCycles\", 418)\n\tassertAttrValue(t, deviceData.Attributes, \"ReadGigabytesProcessed\", 3641)\n\tassertAttrValue(t, deviceData.Attributes, \"WriteGigabytesProcessed\", 2124590)\n\tassertAttrValue(t, deviceData.Attributes, \"VerifyGigabytesProcessed\", 0)\n}\n\nfunc TestParseSmartForSata(t *testing.T) {\n\tfixturePath := filepath.Join(\"test-data\", \"smart\", \"sda.json\")\n\tdata, err := os.ReadFile(fixturePath)\n\trequire.NoError(t, err)\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\thasData, exitStatus := sm.parseSmartForSata(data)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 64, exitStatus)\n\n\tdeviceData, ok := sm.SmartDataMap[\"9C40918040082\"]\n\trequire.True(t, ok, \"expected smart data entry for serial 9C40918040082\")\n\n\tassert.Equal(t, \"P3-2TB\", deviceData.ModelName)\n\tassert.Equal(t, \"X0104A0\", deviceData.FirmwareVersion)\n\tassert.Equal(t, \"/dev/sda\", deviceData.DiskName)\n\tassert.Equal(t, \"sat\", deviceData.DiskType)\n\tassert.Equal(t, uint8(31), deviceData.Temperature)\n\tassert.Equal(t, \"PASSED\", deviceData.SmartStatus)\n\tassert.Equal(t, uint64(2048408248320), deviceData.Capacity)\n\tif assert.NotEmpty(t, deviceData.Attributes) {\n\t\tassertAttrValue(t, deviceData.Attributes, \"Temperature_Celsius\", 31)\n\t}\n}\n\nfunc TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {\n\tjsonPayload := []byte(`{\n\t\t\"smartctl\": {\"exit_status\": 0},\n\t\t\"device\": {\"name\": \"/dev/sdb\", \"type\": \"sat\"},\n\t\t\"model_name\": \"SanDisk SSD U110 16GB\",\n\t\t\"serial_number\": \"DEVSTAT123\",\n\t\t\"firmware_version\": \"U21B001\",\n\t\t\"user_capacity\": {\"bytes\": 16013942784},\n\t\t\"smart_status\": {\"passed\": true},\n\t\t\"ata_smart_attributes\": {\"table\": []},\n\t\t\"ata_device_statistics\": {\n\t\t\t\"pages\": [\n\t\t\t\t{\n\t\t\t\t\t\"number\": 5,\n\t\t\t\t\t\"name\": \"Temperature Statistics\",\n\t\t\t\t\t\"table\": [\n\t\t\t\t\t\t{\"name\": \"Current Temperature\", \"value\": 22, \"flags\": {\"valid\": true}}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}`)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\thasData, exitStatus := sm.parseSmartForSata(jsonPayload)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 0, exitStatus)\n\n\tdeviceData, ok := sm.SmartDataMap[\"DEVSTAT123\"]\n\trequire.True(t, ok, \"expected smart data entry for serial DEVSTAT123\")\n\tassert.Equal(t, uint8(22), deviceData.Temperature)\n}\n\nfunc TestParseSmartForSataAtaDeviceStatistics(t *testing.T) {\n\t// tests that ata_device_statistics values are parsed correctly\n\tjsonPayload := []byte(`{\n\t\t\"smartctl\": {\"exit_status\": 0},\n\t\t\"device\": {\"name\": \"/dev/sdb\", \"type\": \"sat\"},\n\t\t\"model_name\": \"SanDisk SSD U110 16GB\",\n\t\t\"serial_number\": \"lksjfh23lhj\",\n\t\t\"firmware_version\": \"U21B001\",\n\t\t\"user_capacity\": {\"bytes\": 16013942784},\n\t\t\"smart_status\": {\"passed\": true},\n\t\t\"ata_smart_attributes\": {\"table\": []},\n\t\t\"ata_device_statistics\": {\n\t\t\t\"pages\": [\n\t\t\t\t{\n\t\t\t\t\t\"number\": 5,\n\t\t\t\t\t\"name\": \"Temperature Statistics\",\n\t\t\t\t\t\"table\": [\n\t\t\t\t\t\t{\"name\": \"Current Temperature\", \"value\": 43, \"flags\": {\"valid\": true}},\n\t\t\t\t\t\t{\"name\": \"Specified Minimum Operating Temperature\", \"value\": -20, \"flags\": {\"valid\": true}}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}`)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\thasData, exitStatus := sm.parseSmartForSata(jsonPayload)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 0, exitStatus)\n\n\tdeviceData, ok := sm.SmartDataMap[\"lksjfh23lhj\"]\n\trequire.True(t, ok, \"expected smart data entry for serial lksjfh23lhj\")\n\tassert.Equal(t, uint8(43), deviceData.Temperature)\n}\n\nfunc TestParseSmartForSataNegativeDeviceStatistics(t *testing.T) {\n\t// Tests that negative values in ata_device_statistics (e.g. min operating temp)\n\t// do not cause the entire SAT parser to fail.\n\tjsonPayload := []byte(`{\n\t\t\"smartctl\": {\"exit_status\": 0},\n\t\t\"device\": {\"name\": \"/dev/sdb\", \"type\": \"sat\"},\n\t\t\"model_name\": \"SanDisk SSD U110 16GB\",\n\t\t\"serial_number\": \"NEGATIVE123\",\n\t\t\"firmware_version\": \"U21B001\",\n\t\t\"user_capacity\": {\"bytes\": 16013942784},\n\t\t\"smart_status\": {\"passed\": true},\n\t\t\"temperature\": {\"current\": 38},\n\t\t\"ata_smart_attributes\": {\"table\": []},\n\t\t\"ata_device_statistics\": {\n\t\t\t\"pages\": [\n\t\t\t\t{\n\t\t\t\t\t\"number\": 5,\n\t\t\t\t\t\"name\": \"Temperature Statistics\",\n\t\t\t\t\t\"table\": [\n\t\t\t\t\t\t{\"name\": \"Current Temperature\", \"value\": 38, \"flags\": {\"valid\": true}},\n\t\t\t\t\t\t{\"name\": \"Specified Minimum Operating Temperature\", \"value\": -20, \"flags\": {\"valid\": true}}\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}`)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\thasData, exitStatus := sm.parseSmartForSata(jsonPayload)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 0, exitStatus)\n\n\tdeviceData, ok := sm.SmartDataMap[\"NEGATIVE123\"]\n\trequire.True(t, ok, \"expected smart data entry for serial NEGATIVE123\")\n\tassert.Equal(t, uint8(38), deviceData.Temperature)\n}\n\nfunc TestParseSmartForSataParentheticalRawValue(t *testing.T) {\n\tjsonPayload := []byte(`{\n\t\t\"smartctl\": {\"exit_status\": 0},\n\t\t\"device\": {\"name\": \"/dev/sdz\", \"type\": \"sat\"},\n\t\t\"model_name\": \"Example\",\n\t\t\"serial_number\": \"PARENTHESES123\",\n\t\t\"firmware_version\": \"1.0\",\n\t\t\"user_capacity\": {\"bytes\": 1024},\n\t\t\"smart_status\": {\"passed\": true},\n\t\t\"temperature\": {\"current\": 25},\n\t\t\"ata_smart_attributes\": {\n\t\t\t\"table\": [\n\t\t\t\t{\n\t\t\t\t\t\"id\": 9,\n\t\t\t\t\t\"name\": \"Power_On_Hours\",\n\t\t\t\t\t\"value\": 93,\n\t\t\t\t\t\"worst\": 55,\n\t\t\t\t\t\"thresh\": 0,\n\t\t\t\t\t\"when_failed\": \"\",\n                    \"raw\": {\n                        \"value\": 57891864217128,\n                        \"string\": \"39925 (212 206 0)\"\n                    }\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}`)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\n\thasData, exitStatus := sm.parseSmartForSata(jsonPayload)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 0, exitStatus)\n\n\tdata, ok := sm.SmartDataMap[\"PARENTHESES123\"]\n\trequire.True(t, ok)\n\trequire.Len(t, data.Attributes, 1)\n\n\tattr := data.Attributes[0]\n\tassert.Equal(t, uint64(39925), attr.RawValue)\n\tassert.Equal(t, \"39925 (212 206 0)\", attr.RawString)\n}\n\nfunc TestParseSmartForNvme(t *testing.T) {\n\tfixturePath := filepath.Join(\"test-data\", \"smart\", \"nvme0.json\")\n\tdata, err := os.ReadFile(fixturePath)\n\trequire.NoError(t, err)\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\thasData, exitStatus := sm.parseSmartForNvme(data)\n\trequire.True(t, hasData)\n\tassert.Equal(t, 0, exitStatus)\n\n\tdeviceData, ok := sm.SmartDataMap[\"2024031600129\"]\n\trequire.True(t, ok, \"expected smart data entry for serial 2024031600129\")\n\n\tassert.Equal(t, \"PELADN 512GB\", deviceData.ModelName)\n\tassert.Equal(t, \"VC2S038E\", deviceData.FirmwareVersion)\n\tassert.Equal(t, \"/dev/nvme0\", deviceData.DiskName)\n\tassert.Equal(t, \"nvme\", deviceData.DiskType)\n\tassert.Equal(t, uint8(61), deviceData.Temperature)\n\tassert.Equal(t, \"PASSED\", deviceData.SmartStatus)\n\tassert.Equal(t, uint64(512110190592), deviceData.Capacity)\n\tif assert.NotEmpty(t, deviceData.Attributes) {\n\t\tassertAttrValue(t, deviceData.Attributes, \"PercentageUsed\", 0)\n\t\tassertAttrValue(t, deviceData.Attributes, \"DataUnitsWritten\", 16040567)\n\t}\n}\n\nfunc TestHasDataForDevice(t *testing.T) {\n\tsm := &SmartManager{\n\t\tSmartDataMap: map[string]*smart.SmartData{\n\t\t\t\"serial-1\": {DiskName: \"/dev/sda\"},\n\t\t\t\"serial-2\": nil,\n\t\t},\n\t}\n\n\tassert.True(t, sm.hasDataForDevice(\"/dev/sda\"))\n\tassert.False(t, sm.hasDataForDevice(\"/dev/sdb\"))\n}\n\nfunc TestDevicesSnapshotReturnsCopy(t *testing.T) {\n\toriginalDevice := &DeviceInfo{Name: \"/dev/sda\"}\n\tsm := &SmartManager{\n\t\tSmartDevices: []*DeviceInfo{\n\t\t\toriginalDevice,\n\t\t\t{Name: \"/dev/sdb\"},\n\t\t},\n\t}\n\n\tsnapshot := sm.devicesSnapshot()\n\trequire.Len(t, snapshot, 2)\n\n\tsm.SmartDevices[0] = &DeviceInfo{Name: \"/dev/sdz\"}\n\tassert.Equal(t, \"/dev/sda\", snapshot[0].Name)\n\n\tsnapshot[1] = &DeviceInfo{Name: \"/dev/nvme0\"}\n\tassert.Equal(t, \"/dev/sdb\", sm.SmartDevices[1].Name)\n\n\tsm.SmartDevices = append(sm.SmartDevices, &DeviceInfo{Name: \"/dev/nvme1\"})\n\tassert.Len(t, snapshot, 2)\n}\n\nfunc TestScanDevicesWithEnvOverrideAndSeparator(t *testing.T) {\n\tt.Setenv(\"SMART_DEVICES_SEPARATOR\", \"|\")\n\tt.Setenv(\"SMART_DEVICES\", \"/dev/sda:jmb39x-q,0|/dev/nvme0:nvme\")\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\terr := sm.ScanDevices(true)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, sm.SmartDevices, 2)\n\tassert.Equal(t, \"/dev/sda\", sm.SmartDevices[0].Name)\n\tassert.Equal(t, \"jmb39x-q,0\", sm.SmartDevices[0].Type)\n\tassert.Equal(t, \"/dev/nvme0\", sm.SmartDevices[1].Name)\n\tassert.Equal(t, \"nvme\", sm.SmartDevices[1].Type)\n}\n\nfunc TestScanDevicesWithEnvOverride(t *testing.T) {\n\tt.Setenv(\"SMART_DEVICES\", \"/dev/sda:sat, /dev/nvme0:nvme\")\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\terr := sm.ScanDevices(true)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, sm.SmartDevices, 2)\n\tassert.Equal(t, \"/dev/sda\", sm.SmartDevices[0].Name)\n\tassert.Equal(t, \"sat\", sm.SmartDevices[0].Type)\n\tassert.Equal(t, \"/dev/nvme0\", sm.SmartDevices[1].Name)\n\tassert.Equal(t, \"nvme\", sm.SmartDevices[1].Type)\n}\n\nfunc TestScanDevicesWithEnvOverrideInvalid(t *testing.T) {\n\tt.Setenv(\"SMART_DEVICES\", \":sat\")\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\terr := sm.ScanDevices(true)\n\trequire.Error(t, err)\n}\n\nfunc TestScanDevicesWithEnvOverrideEmpty(t *testing.T) {\n\tt.Setenv(\"SMART_DEVICES\", \"   \")\n\n\tsm := &SmartManager{\n\t\tSmartDataMap: make(map[string]*smart.SmartData),\n\t}\n\n\terr := sm.ScanDevices(true)\n\tassert.ErrorIs(t, err, errNoValidSmartData)\n\tassert.Empty(t, sm.SmartDevices)\n}\n\nfunc TestSmartctlArgsWithoutType(t *testing.T) {\n\tdevice := &DeviceInfo{Name: \"/dev/sda\"}\n\n\tsm := &SmartManager{}\n\n\targs := sm.smartctlArgs(device, true)\n\tassert.Equal(t, []string{\"-a\", \"--json=c\", \"-n\", \"standby\", \"/dev/sda\"}, args)\n}\n\nfunc TestSmartctlArgs(t *testing.T) {\n\tsm := &SmartManager{}\n\n\tsataDevice := &DeviceInfo{Name: \"/dev/sda\", Type: \"sat\"}\n\tassert.Equal(t,\n\t\t[]string{\"-d\", \"sat\", \"-a\", \"--json=c\", \"-l\", \"devstat\", \"-n\", \"standby\", \"/dev/sda\"},\n\t\tsm.smartctlArgs(sataDevice, true),\n\t)\n\n\tassert.Equal(t,\n\t\t[]string{\"-d\", \"sat\", \"-a\", \"--json=c\", \"-l\", \"devstat\", \"/dev/sda\"},\n\t\tsm.smartctlArgs(sataDevice, false),\n\t)\n\n\tnvmeDevice := &DeviceInfo{Name: \"/dev/nvme0\", Type: \"nvme\"}\n\tassert.Equal(t,\n\t\t[]string{\"-d\", \"nvme\", \"-a\", \"--json=c\", \"-n\", \"standby\", \"/dev/nvme0\"},\n\t\tsm.smartctlArgs(nvmeDevice, true),\n\t)\n\n\tassert.Equal(t,\n\t\t[]string{\"-a\", \"--json=c\", \"-n\", \"standby\"},\n\t\tsm.smartctlArgs(nil, true),\n\t)\n}\n\nfunc TestResolveRefreshError(t *testing.T) {\n\tscanErr := errors.New(\"scan failed\")\n\tcollectErr := errors.New(\"collect failed\")\n\n\ttests := []struct {\n\t\tname        string\n\t\tdevices     []*DeviceInfo\n\t\tdata        map[string]*smart.SmartData\n\t\tscanErr     error\n\t\tcollectErr  error\n\t\texpectedErr error\n\t\texpectNoErr bool\n\t}{\n\t\t{\n\t\t\tname:        \"no devices returns scan error\",\n\t\t\tdevices:     nil,\n\t\t\tdata:        make(map[string]*smart.SmartData),\n\t\t\tscanErr:     scanErr,\n\t\t\texpectedErr: scanErr,\n\t\t},\n\t\t{\n\t\t\tname:        \"has data ignores errors\",\n\t\t\tdevices:     []*DeviceInfo{{Name: \"/dev/sda\"}},\n\t\t\tdata:        map[string]*smart.SmartData{\"serial\": {}},\n\t\t\tscanErr:     scanErr,\n\t\t\tcollectErr:  collectErr,\n\t\t\texpectNoErr: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"collect error preferred\",\n\t\t\tdevices:     []*DeviceInfo{{Name: \"/dev/sda\"}},\n\t\t\tdata:        make(map[string]*smart.SmartData),\n\t\t\tcollectErr:  collectErr,\n\t\t\texpectedErr: collectErr,\n\t\t},\n\t\t{\n\t\t\tname:        \"scan error returned when no data\",\n\t\t\tdevices:     []*DeviceInfo{{Name: \"/dev/sda\"}},\n\t\t\tdata:        make(map[string]*smart.SmartData),\n\t\t\tscanErr:     scanErr,\n\t\t\texpectedErr: scanErr,\n\t\t},\n\t\t{\n\t\t\tname:        \"no errors returns sentinel\",\n\t\t\tdevices:     []*DeviceInfo{{Name: \"/dev/sda\"}},\n\t\t\tdata:        make(map[string]*smart.SmartData),\n\t\t\texpectedErr: errNoValidSmartData,\n\t\t},\n\t\t{\n\t\t\tname:        \"no devices collect error\",\n\t\t\tdevices:     nil,\n\t\t\tdata:        make(map[string]*smart.SmartData),\n\t\t\tcollectErr:  collectErr,\n\t\t\texpectedErr: collectErr,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsm := &SmartManager{\n\t\t\t\tSmartDevices: tt.devices,\n\t\t\t\tSmartDataMap: tt.data,\n\t\t\t}\n\n\t\t\terr := sm.resolveRefreshError(tt.scanErr, tt.collectErr)\n\t\t\tif tt.expectNoErr {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectedErr == nil {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.expectedErr, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseScan(t *testing.T) {\n\tsm := &SmartManager{\n\t\tSmartDataMap: map[string]*smart.SmartData{\n\t\t\t\"serial-active\": {DiskName: \"/dev/sda\"},\n\t\t\t\"serial-stale\":  {DiskName: \"/dev/sdb\"},\n\t\t},\n\t}\n\n\tscanJSON := []byte(`{\n        \"devices\": [\n            {\"name\": \"/dev/sda\", \"type\": \"sat\", \"info_name\": \"/dev/sda [SAT]\", \"protocol\": \"ATA\"},\n            {\"name\": \"/dev/nvme0\", \"type\": \"nvme\", \"info_name\": \"/dev/nvme0\", \"protocol\": \"NVMe\"}\n        ]\n    }`)\n\n\tdevices, hasData := sm.parseScan(scanJSON)\n\tassert.True(t, hasData)\n\n\tsm.updateSmartDevices(devices)\n\n\trequire.Len(t, sm.SmartDevices, 2)\n\tassert.Equal(t, \"/dev/sda\", sm.SmartDevices[0].Name)\n\tassert.Equal(t, \"sat\", sm.SmartDevices[0].Type)\n\tassert.Equal(t, \"/dev/nvme0\", sm.SmartDevices[1].Name)\n\tassert.Equal(t, \"nvme\", sm.SmartDevices[1].Type)\n\n\t_, activeExists := sm.SmartDataMap[\"serial-active\"]\n\tassert.True(t, activeExists, \"active smart data should be preserved when device path remains\")\n\n\t_, staleExists := sm.SmartDataMap[\"serial-stale\"]\n\tassert.False(t, staleExists, \"stale smart data entry should be removed when device path disappears\")\n}\n\nfunc TestMergeDeviceListsPrefersConfigured(t *testing.T) {\n\tscanned := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"sat\", InfoName: \"scan-info\", Protocol: \"ATA\"},\n\t\t{Name: \"/dev/nvme0\", Type: \"nvme\"},\n\t}\n\n\tconfigured := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"sat-override\"},\n\t\t{Name: \"/dev/sdb\", Type: \"sat\"},\n\t}\n\n\tmerged := mergeDeviceLists(nil, scanned, configured)\n\trequire.Len(t, merged, 3)\n\n\tbyName := make(map[string]*DeviceInfo, len(merged))\n\tfor _, dev := range merged {\n\t\tbyName[dev.Name] = dev\n\t}\n\n\trequire.Contains(t, byName, \"/dev/sda\")\n\tassert.Equal(t, \"sat-override\", byName[\"/dev/sda\"].Type, \"configured type should override scanned type\")\n\tassert.Equal(t, \"scan-info\", byName[\"/dev/sda\"].InfoName, \"scan metadata should be preserved when config does not provide it\")\n\n\trequire.Contains(t, byName, \"/dev/nvme0\")\n\tassert.Equal(t, \"nvme\", byName[\"/dev/nvme0\"].Type)\n\n\trequire.Contains(t, byName, \"/dev/sdb\")\n\tassert.Equal(t, \"sat\", byName[\"/dev/sdb\"].Type)\n}\n\nfunc TestMergeDeviceListsPreservesVerification(t *testing.T) {\n\texisting := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"sat+megaraid\", parserType: \"sat\", typeVerified: true},\n\t}\n\n\tscanned := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"nvme\"},\n\t}\n\n\tmerged := mergeDeviceLists(existing, scanned, nil)\n\trequire.Len(t, merged, 1)\n\n\tdevice := merged[0]\n\tassert.True(t, device.typeVerified)\n\tassert.Equal(t, \"sat\", device.parserType)\n\tassert.Equal(t, \"sat+megaraid\", device.Type)\n}\n\nfunc TestMergeDeviceListsUpdatesTypeWhenUnverified(t *testing.T) {\n\texisting := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"sat\", parserType: \"sat\", typeVerified: false},\n\t}\n\n\tscanned := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"nvme\"},\n\t}\n\n\tmerged := mergeDeviceLists(existing, scanned, nil)\n\trequire.Len(t, merged, 1)\n\n\tdevice := merged[0]\n\tassert.False(t, device.typeVerified)\n\tassert.Equal(t, \"nvme\", device.Type)\n\tassert.Equal(t, \"\", device.parserType)\n}\n\nfunc TestMergeDeviceListsHandlesDevicesWithSameNameAndDifferentTypes(t *testing.T) {\n\t// There are use cases where the same device name is re-used,\n\t// for example, a RAID controller with multiple drives.\n\tscanned := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"megaraid,0\"},\n\t\t{Name: \"/dev/sda\", Type: \"megaraid,1\"},\n\t\t{Name: \"/dev/sda\", Type: \"megaraid,2\"},\n\t}\n\n\tmerged := mergeDeviceLists(nil, scanned, nil)\n\trequire.Len(t, merged, 3, \"should have 3 separate devices for RAID controller\")\n\n\tbyKey := make(map[string]*DeviceInfo, len(merged))\n\tfor _, dev := range merged {\n\t\tkey := dev.Name + \"|\" + dev.Type\n\t\tbyKey[key] = dev\n\t}\n\n\tassert.Contains(t, byKey, \"/dev/sda|megaraid,0\")\n\tassert.Contains(t, byKey, \"/dev/sda|megaraid,1\")\n\tassert.Contains(t, byKey, \"/dev/sda|megaraid,2\")\n}\n\nfunc TestMergeDeviceListsHandlesMixedRAIDAndRegular(t *testing.T) {\n\t// Test mixing RAID drives with regular devices\n\tscanned := []*DeviceInfo{\n\t\t{Name: \"/dev/sda\", Type: \"megaraid,0\"},\n\t\t{Name: \"/dev/sda\", Type: \"megaraid,1\"},\n\t\t{Name: \"/dev/sdb\", Type: \"sat\"},\n\t\t{Name: \"/dev/nvme0\", Type: \"nvme\"},\n\t}\n\n\tmerged := mergeDeviceLists(nil, scanned, nil)\n\trequire.Len(t, merged, 4, \"should have 4 separate devices\")\n\n\tbyKey := make(map[string]*DeviceInfo, len(merged))\n\tfor _, dev := range merged {\n\t\tkey := dev.Name + \"|\" + dev.Type\n\t\tbyKey[key] = dev\n\t}\n\n\tassert.Contains(t, byKey, \"/dev/sda|megaraid,0\")\n\tassert.Contains(t, byKey, \"/dev/sda|megaraid,1\")\n\tassert.Contains(t, byKey, \"/dev/sdb|sat\")\n\tassert.Contains(t, byKey, \"/dev/nvme0|nvme\")\n}\n\nfunc TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) {\n\t// Test that updateSmartDevices correctly validates RAID drives using composite keys\n\tsm := &SmartManager{\n\t\tSmartDevices: []*DeviceInfo{\n\t\t\t{Name: \"/dev/sda\", Type: \"megaraid,0\"},\n\t\t\t{Name: \"/dev/sda\", Type: \"megaraid,1\"},\n\t\t},\n\t\tSmartDataMap: map[string]*smart.SmartData{\n\t\t\t\"serial-0\": {\n\t\t\t\tDiskName:     \"/dev/sda\",\n\t\t\t\tDiskType:     \"megaraid,0\",\n\t\t\t\tSerialNumber: \"serial-0\",\n\t\t\t},\n\t\t\t\"serial-1\": {\n\t\t\t\tDiskName:     \"/dev/sda\",\n\t\t\t\tDiskType:     \"megaraid,1\",\n\t\t\t\tSerialNumber: \"serial-1\",\n\t\t\t},\n\t\t\t\"serial-stale\": {\n\t\t\t\tDiskName:     \"/dev/sda\",\n\t\t\t\tDiskType:     \"megaraid,2\",\n\t\t\t\tSerialNumber: \"serial-stale\",\n\t\t\t},\n\t\t},\n\t}\n\n\tsm.updateSmartDevices(sm.SmartDevices)\n\n\t// serial-0 and serial-1 should be preserved (matching devices exist)\n\tassert.Contains(t, sm.SmartDataMap, \"serial-0\")\n\tassert.Contains(t, sm.SmartDataMap, \"serial-1\")\n\t// serial-stale should be removed (no matching device)\n\tassert.NotContains(t, sm.SmartDataMap, \"serial-stale\")\n}\n\nfunc TestParseSmartOutputMarksVerified(t *testing.T) {\n\tfixturePath := filepath.Join(\"test-data\", \"smart\", \"nvme0.json\")\n\tdata, err := os.ReadFile(fixturePath)\n\trequire.NoError(t, err)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\tdevice := &DeviceInfo{Name: \"/dev/nvme0\"}\n\n\trequire.True(t, sm.parseSmartOutput(device, data))\n\tassert.Equal(t, \"nvme\", device.Type)\n\tassert.Equal(t, \"nvme\", device.parserType)\n\tassert.True(t, device.typeVerified)\n}\n\nfunc TestParseSmartOutputKeepsCustomType(t *testing.T) {\n\tfixturePath := filepath.Join(\"test-data\", \"smart\", \"sda.json\")\n\tdata, err := os.ReadFile(fixturePath)\n\trequire.NoError(t, err)\n\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\tdevice := &DeviceInfo{Name: \"/dev/sda\", Type: \"sat+megaraid\"}\n\n\trequire.True(t, sm.parseSmartOutput(device, data))\n\tassert.Equal(t, \"sat+megaraid\", device.Type)\n\tassert.Equal(t, \"sat\", device.parserType)\n\tassert.True(t, device.typeVerified)\n}\n\nfunc TestParseSmartOutputResetsVerificationOnFailure(t *testing.T) {\n\tsm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}\n\tdevice := &DeviceInfo{Name: \"/dev/sda\", Type: \"sat\", parserType: \"sat\", typeVerified: true}\n\n\tassert.False(t, sm.parseSmartOutput(device, []byte(\"not json\")))\n\tassert.False(t, device.typeVerified)\n\tassert.Equal(t, \"sat\", device.parserType)\n}\n\nfunc assertAttrValue(t *testing.T, attributes []*smart.SmartAttribute, name string, expected uint64) {\n\tt.Helper()\n\tattr := findAttr(attributes, name)\n\tif attr == nil {\n\t\tt.Fatalf(\"expected attribute %s to be present\", name)\n\t}\n\tif attr.RawValue != expected {\n\t\tt.Fatalf(\"unexpected attribute %s value: got %d, want %d\", name, attr.RawValue, expected)\n\t}\n}\n\nfunc findAttr(attributes []*smart.SmartAttribute, name string) *smart.SmartAttribute {\n\tfor _, attr := range attributes {\n\t\tif attr != nil && attr.Name == name {\n\t\t\treturn attr\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc TestIsVirtualDevice(t *testing.T) {\n\tsm := &SmartManager{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tvendor   string\n\t\tproduct  string\n\t\tmodel    string\n\t\texpected bool\n\t}{\n\t\t{\"regular drive\", \"SEAGATE\", \"ST1000DM003\", \"ST1000DM003-1CH162\", false},\n\t\t{\"qemu virtual\", \"QEMU\", \"QEMU HARDDISK\", \"QEMU HARDDISK\", true},\n\t\t{\"virtualbox virtual\", \"VBOX\", \"HARDDISK\", \"VBOX HARDDISK\", true},\n\t\t{\"vmware virtual\", \"VMWARE\", \"Virtual disk\", \"VMWARE Virtual disk\", true},\n\t\t{\"virtual in model\", \"ATA\", \"VIRTUAL\", \"VIRTUAL DISK\", true},\n\t\t{\"iet virtual\", \"IET\", \"VIRTUAL-DISK\", \"VIRTUAL-DISK\", true},\n\t\t{\"hyper-v virtual\", \"MSFT\", \"VIRTUAL HD\", \"VIRTUAL HD\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdata := &smart.SmartInfoForSata{\n\t\t\t\tScsiVendor:  tt.vendor,\n\t\t\t\tScsiProduct: tt.product,\n\t\t\t\tModelName:   tt.model,\n\t\t\t}\n\t\t\tresult := sm.isVirtualDevice(data)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsVirtualDeviceNvme(t *testing.T) {\n\tsm := &SmartManager{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tmodel    string\n\t\texpected bool\n\t}{\n\t\t{\"regular nvme\", \"Samsung SSD 970 EVO Plus 1TB\", false},\n\t\t{\"qemu virtual\", \"QEMU NVMe Ctrl\", true},\n\t\t{\"virtualbox virtual\", \"VBOX NVMe\", true},\n\t\t{\"vmware virtual\", \"VMWARE NVMe\", true},\n\t\t{\"virtual in model\", \"Virtual NVMe Device\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdata := &smart.SmartInfoForNvme{\n\t\t\t\tModelName: tt.model,\n\t\t\t}\n\t\t\tresult := sm.isVirtualDeviceNvme(data)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsVirtualDeviceScsi(t *testing.T) {\n\tsm := &SmartManager{}\n\n\ttests := []struct {\n\t\tname     string\n\t\tvendor   string\n\t\tproduct  string\n\t\tmodel    string\n\t\texpected bool\n\t}{\n\t\t{\"regular scsi\", \"SEAGATE\", \"ST1000DM003\", \"ST1000DM003-1CH162\", false},\n\t\t{\"qemu virtual\", \"QEMU\", \"QEMU HARDDISK\", \"QEMU HARDDISK\", true},\n\t\t{\"virtualbox virtual\", \"VBOX\", \"HARDDISK\", \"VBOX HARDDISK\", true},\n\t\t{\"vmware virtual\", \"VMWARE\", \"Virtual disk\", \"VMWARE Virtual disk\", true},\n\t\t{\"virtual in model\", \"ATA\", \"VIRTUAL\", \"VIRTUAL DISK\", true},\n\t\t{\"iet virtual\", \"IET\", \"VIRTUAL-DISK\", \"VIRTUAL-DISK\", true},\n\t\t{\"hyper-v virtual\", \"MSFT\", \"VIRTUAL HD\", \"VIRTUAL HD\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdata := &smart.SmartInfoForScsi{\n\t\t\t\tScsiVendor:    tt.vendor,\n\t\t\t\tScsiProduct:   tt.product,\n\t\t\t\tScsiModelName: tt.model,\n\t\t\t}\n\t\t\tresult := sm.isVirtualDeviceScsi(data)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestFindAtaDeviceStatisticsValue(t *testing.T) {\n\tval42 := int64(42)\n\tval100 := int64(100)\n\tvalMinus20 := int64(-20)\n\n\ttests := []struct {\n\t\tname           string\n\t\tdata           smart.SmartInfoForSata\n\t\tataDeviceStats smart.AtaDeviceStatistics\n\t\tentryNumber    uint8\n\t\tentryName      string\n\t\tminValue       int64\n\t\tmaxValue       int64\n\t\texpectedValue  *int64\n\t}{\n\t\t{\n\t\t\tname: \"value in ataDeviceStats\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Current Temperature\", Value: &val42},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: &val42,\n\t\t},\n\t\t{\n\t\t\tname: \"value unmarshaled from data\",\n\t\t\tdata: smart.SmartInfoForSata{\n\t\t\t\tAtaDeviceStatistics: []byte(`{\"pages\":[{\"number\":5,\"table\":[{\"name\":\"Current Temperature\",\"value\":100}]}]}`),\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      255,\n\t\t\texpectedValue: &val100,\n\t\t},\n\t\t{\n\t\t\tname: \"value out of range (too high)\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Current Temperature\", Value: &val100},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      50,\n\t\t\texpectedValue: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"value out of range (too low)\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Min Temp\", Value: &valMinus20},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Min Temp\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: nil,\n\t\t},\n\t\t{\n\t\t\tname:          \"no statistics available\",\n\t\t\tdata:          smart.SmartInfoForSata{},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      255,\n\t\t\texpectedValue: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong page number\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 1,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Current Temperature\", Value: &val42},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong entry name\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Other Stat\", Value: &val42},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"case insensitive name match\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"CURRENT TEMPERATURE\", Value: &val42},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: &val42,\n\t\t},\n\t\t{\n\t\t\tname: \"entry value is nil\",\n\t\t\tataDeviceStats: smart.AtaDeviceStatistics{\n\t\t\t\tPages: []smart.AtaDeviceStatisticsPage{\n\t\t\t\t\t{\n\t\t\t\t\t\tNumber: 5,\n\t\t\t\t\t\tTable: []smart.AtaDeviceStatisticsEntry{\n\t\t\t\t\t\t\t{Name: \"Current Temperature\", Value: nil},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tentryNumber:   5,\n\t\t\tentryName:     \"Current Temperature\",\n\t\t\tminValue:      0,\n\t\t\tmaxValue:      100,\n\t\t\texpectedValue: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := findAtaDeviceStatisticsValue(&tt.data, &tt.ataDeviceStats, tt.entryNumber, tt.entryName, tt.minValue, tt.maxValue)\n\t\t\tif tt.expectedValue == nil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tassert.Equal(t, *tt.expectedValue, *result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRefreshExcludedDevices(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tenvValue     string\n\t\texpectedDevs map[string]struct{}\n\t}{\n\t\t{\n\t\t\tname:         \"empty env\",\n\t\t\tenvValue:     \"\",\n\t\t\texpectedDevs: map[string]struct{}{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single device\",\n\t\t\tenvValue: \"/dev/sda\",\n\t\t\texpectedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\": {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple devices\",\n\t\t\tenvValue: \"/dev/sda,/dev/sdb,/dev/nvme0\",\n\t\t\texpectedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\":   {},\n\t\t\t\t\"/dev/sdb\":   {},\n\t\t\t\t\"/dev/nvme0\": {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"devices with whitespace\",\n\t\t\tenvValue: \" /dev/sda , /dev/sdb ,  /dev/nvme0  \",\n\t\t\texpectedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\":   {},\n\t\t\t\t\"/dev/sdb\":   {},\n\t\t\t\t\"/dev/nvme0\": {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"duplicate devices\",\n\t\t\tenvValue: \"/dev/sda,/dev/sdb,/dev/sda\",\n\t\t\texpectedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\": {},\n\t\t\t\t\"/dev/sdb\": {},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty entries and whitespace\",\n\t\t\tenvValue: \"/dev/sda,, /dev/sdb , , \",\n\t\t\texpectedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\": {},\n\t\t\t\t\"/dev/sdb\": {},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.envValue != \"\" {\n\t\t\t\tt.Setenv(\"EXCLUDE_SMART\", tt.envValue)\n\t\t\t} else {\n\t\t\t\t// Ensure env var is not set for empty test\n\t\t\t\tos.Unsetenv(\"EXCLUDE_SMART\")\n\t\t\t}\n\n\t\t\tsm := &SmartManager{}\n\t\t\tsm.refreshExcludedDevices()\n\n\t\t\tassert.Equal(t, tt.expectedDevs, sm.excludedDevices)\n\t\t})\n\t}\n}\n\nfunc TestIsExcludedDevice(t *testing.T) {\n\tsm := &SmartManager{\n\t\texcludedDevices: map[string]struct{}{\n\t\t\t\"/dev/sda\":   {},\n\t\t\t\"/dev/nvme0\": {},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tdeviceName   string\n\t\texpectedBool bool\n\t}{\n\t\t{\"excluded device sda\", \"/dev/sda\", true},\n\t\t{\"excluded device nvme0\", \"/dev/nvme0\", true},\n\t\t{\"non-excluded device sdb\", \"/dev/sdb\", false},\n\t\t{\"non-excluded device nvme1\", \"/dev/nvme1\", false},\n\t\t{\"empty device name\", \"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := sm.isExcludedDevice(tt.deviceName)\n\t\t\tassert.Equal(t, tt.expectedBool, result)\n\t\t})\n\t}\n}\n\nfunc TestFilterExcludedDevices(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\texcludedDevs   map[string]struct{}\n\t\tinputDevices   []*DeviceInfo\n\t\texpectedDevs   []*DeviceInfo\n\t\texpectedLength int\n\t}{\n\t\t{\n\t\t\tname:         \"no exclusions\",\n\t\t\texcludedDevs: map[string]struct{}{},\n\t\t\tinputDevices: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sda\"},\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t\t{Name: \"/dev/nvme0\"},\n\t\t\t},\n\t\t\texpectedDevs: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sda\"},\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t\t{Name: \"/dev/nvme0\"},\n\t\t\t},\n\t\t\texpectedLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"some devices excluded\",\n\t\t\texcludedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\":   {},\n\t\t\t\t\"/dev/nvme0\": {},\n\t\t\t},\n\t\t\tinputDevices: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sda\"},\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t\t{Name: \"/dev/nvme0\"},\n\t\t\t\t{Name: \"/dev/nvme1\"},\n\t\t\t},\n\t\t\texpectedDevs: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t\t{Name: \"/dev/nvme1\"},\n\t\t\t},\n\t\t\texpectedLength: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"all devices excluded\",\n\t\t\texcludedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\": {},\n\t\t\t\t\"/dev/sdb\": {},\n\t\t\t},\n\t\t\tinputDevices: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sda\"},\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t},\n\t\t\texpectedDevs:   []*DeviceInfo{},\n\t\t\texpectedLength: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"nil devices\",\n\t\t\texcludedDevs:   map[string]struct{}{},\n\t\t\tinputDevices:   nil,\n\t\t\texpectedDevs:   []*DeviceInfo{},\n\t\t\texpectedLength: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"filter nil and empty name devices\",\n\t\t\texcludedDevs: map[string]struct{}{\n\t\t\t\t\"/dev/sda\": {},\n\t\t\t},\n\t\t\tinputDevices: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sda\"},\n\t\t\t\tnil,\n\t\t\t\t{Name: \"\"},\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t},\n\t\t\texpectedDevs: []*DeviceInfo{\n\t\t\t\t{Name: \"/dev/sdb\"},\n\t\t\t},\n\t\t\texpectedLength: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsm := &SmartManager{\n\t\t\t\texcludedDevices: tt.excludedDevs,\n\t\t\t}\n\n\t\t\tresult := sm.filterExcludedDevices(tt.inputDevices)\n\n\t\t\tassert.Len(t, result, tt.expectedLength)\n\t\t\tassert.Equal(t, tt.expectedDevs, result)\n\t\t})\n\t}\n}\n\nfunc TestIsNvmeControllerPath(t *testing.T) {\n\ttests := []struct {\n\t\tpath     string\n\t\texpected bool\n\t}{\n\t\t// Controller paths (should return true)\n\t\t{\"/dev/nvme0\", true},\n\t\t{\"/dev/nvme1\", true},\n\t\t{\"/dev/nvme10\", true},\n\t\t{\"nvme0\", true},\n\n\t\t// Namespace paths (should return false)\n\t\t{\"/dev/nvme0n1\", false},\n\t\t{\"/dev/nvme1n1\", false},\n\t\t{\"/dev/nvme0n1p1\", false},\n\t\t{\"nvme0n1\", false},\n\n\t\t// Non-NVMe paths (should return false)\n\t\t{\"/dev/sda\", false},\n\t\t{\"/dev/sda1\", false},\n\t\t{\"/dev/hda\", false},\n\t\t{\"\", false},\n\t\t{\"/dev/nvme\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.path, func(t *testing.T) {\n\t\t\tresult := isNvmeControllerPath(tt.path)\n\t\t\tassert.Equal(t, tt.expected, result, \"path: %s\", tt.path)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "agent/smart_windows.go",
    "content": "//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/smartctl.exe\nvar embeddedSmartctl []byte\n\nvar (\n\tsmartctlOnce sync.Once\n\tsmartctlPath string\n\tsmartctlErr  error\n)\n\nfunc ensureEmbeddedSmartctl() (string, error) {\n\tsmartctlOnce.Do(func() {\n\t\tdestDir := filepath.Join(os.TempDir(), \"beszel\", \"smartmontools\")\n\t\tif err := os.MkdirAll(destDir, 0o755); err != nil {\n\t\t\tsmartctlErr = fmt.Errorf(\"failed to create smartctl directory: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tdestPath := filepath.Join(destDir, \"smartctl.exe\")\n\t\tif err := os.WriteFile(destPath, embeddedSmartctl, 0o755); err != nil {\n\t\t\tsmartctlErr = fmt.Errorf(\"failed to write embedded smartctl: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tsmartctlPath = destPath\n\t})\n\n\treturn smartctlPath, smartctlErr\n}\n"
  },
  {
    "path": "agent/system.go",
    "content": "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/beszel\"\n\t\"github.com/henrygd/beszel/agent/battery\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/agent/zfs\"\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/shirou/gopsutil/v4/cpu\"\n\t\"github.com/shirou/gopsutil/v4/host\"\n\t\"github.com/shirou/gopsutil/v4/load\"\n\t\"github.com/shirou/gopsutil/v4/mem\"\n)\n\n// prevDisk stores previous per-device disk counters for a given cache interval\ntype prevDisk struct {\n\treadBytes  uint64\n\twriteBytes uint64\n\tat         time.Time\n}\n\n// Sets initial / non-changing values about the host system\nfunc (a *Agent) refreshSystemDetails() {\n\ta.systemInfo.AgentVersion = beszel.Version\n\n\t// get host info from Docker if available\n\tvar hostInfo container.HostInfo\n\n\tif a.dockerManager != nil {\n\t\ta.systemDetails.Podman = a.dockerManager.IsPodman()\n\t\thostInfo, _ = a.dockerManager.GetHostInfo()\n\t}\n\n\ta.systemDetails.Hostname, _ = os.Hostname()\n\tif arch, err := host.KernelArch(); err == nil {\n\t\ta.systemDetails.Arch = arch\n\t} else {\n\t\ta.systemDetails.Arch = runtime.GOARCH\n\t}\n\n\tplatform, _, version, _ := host.PlatformInformation()\n\n\tif platform == \"darwin\" {\n\t\ta.systemDetails.Os = system.Darwin\n\t\ta.systemDetails.OsName = fmt.Sprintf(\"macOS %s\", version)\n\t} else if strings.Contains(platform, \"indows\") {\n\t\ta.systemDetails.Os = system.Windows\n\t\ta.systemDetails.OsName = strings.Replace(platform, \"Microsoft \", \"\", 1)\n\t\ta.systemDetails.Kernel = version\n\t} else if platform == \"freebsd\" {\n\t\ta.systemDetails.Os = system.Freebsd\n\t\ta.systemDetails.Kernel, _ = host.KernelVersion()\n\t\tif prettyName, err := getOsPrettyName(); err == nil {\n\t\t\ta.systemDetails.OsName = prettyName\n\t\t} else {\n\t\t\ta.systemDetails.OsName = \"FreeBSD\"\n\t\t}\n\t} else {\n\t\ta.systemDetails.Os = system.Linux\n\t\ta.systemDetails.OsName = hostInfo.OperatingSystem\n\t\tif a.systemDetails.OsName == \"\" {\n\t\t\tif prettyName, err := getOsPrettyName(); err == nil {\n\t\t\t\ta.systemDetails.OsName = prettyName\n\t\t\t} else {\n\t\t\t\ta.systemDetails.OsName = platform\n\t\t\t}\n\t\t}\n\t\ta.systemDetails.Kernel = hostInfo.KernelVersion\n\t\tif a.systemDetails.Kernel == \"\" {\n\t\t\ta.systemDetails.Kernel, _ = host.KernelVersion()\n\t\t}\n\t}\n\n\t// cpu model\n\tif info, err := cpu.Info(); err == nil && len(info) > 0 {\n\t\ta.systemDetails.CpuModel = info[0].ModelName\n\t}\n\t// cores / threads\n\tcores, _ := cpu.Counts(false)\n\tthreads := hostInfo.NCPU\n\tif threads == 0 {\n\t\tthreads, _ = cpu.Counts(true)\n\t}\n\t// in lxc, logical cores reflects container limits, so use that as cores if lower\n\tif threads > 0 && threads < cores {\n\t\tcores = threads\n\t}\n\ta.systemDetails.Cores = cores\n\ta.systemDetails.Threads = threads\n\n\t// total memory\n\ta.systemDetails.MemoryTotal = hostInfo.MemTotal\n\tif a.systemDetails.MemoryTotal == 0 {\n\t\tif v, err := mem.VirtualMemory(); err == nil {\n\t\t\ta.systemDetails.MemoryTotal = v.Total\n\t\t}\n\t}\n\n\t// zfs\n\tif _, err := zfs.ARCSize(); err != nil {\n\t\tslog.Debug(\"Not monitoring ZFS ARC\", \"err\", err)\n\t} else {\n\t\ta.zfs = true\n\t}\n}\n\n// Returns current info, stats about the host system\nfunc (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {\n\tvar systemStats system.Stats\n\n\t// battery\n\tif batteryPercent, batteryState, err := battery.GetBatteryStats(); err == nil {\n\t\tsystemStats.Battery[0] = batteryPercent\n\t\tsystemStats.Battery[1] = batteryState\n\t}\n\n\t// cpu metrics\n\tcpuMetrics, err := getCpuMetrics(cacheTimeMs)\n\tif err == nil {\n\t\tsystemStats.Cpu = utils.TwoDecimals(cpuMetrics.Total)\n\t\tsystemStats.CpuBreakdown = []float64{\n\t\t\tutils.TwoDecimals(cpuMetrics.User),\n\t\t\tutils.TwoDecimals(cpuMetrics.System),\n\t\t\tutils.TwoDecimals(cpuMetrics.Iowait),\n\t\t\tutils.TwoDecimals(cpuMetrics.Steal),\n\t\t\tutils.TwoDecimals(cpuMetrics.Idle),\n\t\t}\n\t} else {\n\t\tslog.Error(\"Error getting cpu metrics\", \"err\", err)\n\t}\n\n\t// per-core cpu usage\n\tif perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {\n\t\tsystemStats.CpuCoresUsage = perCoreUsage\n\t}\n\n\t// load average\n\tif avgstat, err := load.Avg(); err == nil {\n\t\tsystemStats.LoadAvg[0] = avgstat.Load1\n\t\tsystemStats.LoadAvg[1] = avgstat.Load5\n\t\tsystemStats.LoadAvg[2] = avgstat.Load15\n\t\tslog.Debug(\"Load average\", \"5m\", avgstat.Load5, \"15m\", avgstat.Load15)\n\t} else {\n\t\tslog.Error(\"Error getting load average\", \"err\", err)\n\t}\n\n\t// memory\n\tif v, err := mem.VirtualMemory(); err == nil {\n\t\t// swap\n\t\tsystemStats.Swap = utils.BytesToGigabytes(v.SwapTotal)\n\t\tsystemStats.SwapUsed = utils.BytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)\n\t\t// cache + buffers value for default mem calculation\n\t\t// note: gopsutil automatically adds SReclaimable to v.Cached\n\t\tcacheBuff := v.Cached + v.Buffers - v.Shared\n\t\tif cacheBuff <= 0 {\n\t\t\tcacheBuff = max(v.Total-v.Free-v.Used, 0)\n\t\t}\n\t\t// htop memory calculation overrides (likely outdated as of mid 2025)\n\t\tif a.memCalc == \"htop\" {\n\t\t\t// cacheBuff = v.Cached + v.Buffers - v.Shared\n\t\t\tv.Used = v.Total - (v.Free + cacheBuff)\n\t\t\tv.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0\n\t\t}\n\t\t// if a.memCalc == \"legacy\" {\n\t\t// \tv.Used = v.Total - v.Free - v.Buffers - v.Cached\n\t\t// \tcacheBuff = v.Total - v.Free - v.Used\n\t\t// \tv.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0\n\t\t// }\n\t\t// subtract ZFS ARC size from used memory and add as its own category\n\t\tif a.zfs {\n\t\t\tif arcSize, _ := zfs.ARCSize(); arcSize > 0 && arcSize < v.Used {\n\t\t\t\tv.Used = v.Used - arcSize\n\t\t\t\tv.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0\n\t\t\t\tsystemStats.MemZfsArc = utils.BytesToGigabytes(arcSize)\n\t\t\t}\n\t\t}\n\t\tsystemStats.Mem = utils.BytesToGigabytes(v.Total)\n\t\tsystemStats.MemBuffCache = utils.BytesToGigabytes(cacheBuff)\n\t\tsystemStats.MemUsed = utils.BytesToGigabytes(v.Used)\n\t\tsystemStats.MemPct = utils.TwoDecimals(v.UsedPercent)\n\t}\n\n\t// disk usage\n\ta.updateDiskUsage(&systemStats)\n\n\t// disk i/o (cache-aware per interval)\n\ta.updateDiskIo(cacheTimeMs, &systemStats)\n\n\t// network stats (per cache interval)\n\ta.updateNetworkStats(cacheTimeMs, &systemStats)\n\n\t// temperatures\n\t// TODO: maybe refactor to methods on systemStats\n\ta.updateTemperatures(&systemStats)\n\n\t// GPU data\n\tif a.gpuManager != nil {\n\t\t// reset high gpu percent\n\t\ta.systemInfo.GpuPct = 0\n\t\t// get current GPU data\n\t\tif gpuData := a.gpuManager.GetCurrentData(cacheTimeMs); len(gpuData) > 0 {\n\t\t\tsystemStats.GPUData = gpuData\n\n\t\t\t// add temperatures\n\t\t\tif systemStats.Temperatures == nil {\n\t\t\t\tsystemStats.Temperatures = make(map[string]float64, len(gpuData))\n\t\t\t}\n\t\t\thighestTemp := 0.0\n\t\t\tfor _, gpu := range gpuData {\n\t\t\t\tif gpu.Temperature > 0 {\n\t\t\t\t\tsystemStats.Temperatures[gpu.Name] = gpu.Temperature\n\t\t\t\t\tif a.sensorConfig.primarySensor == gpu.Name {\n\t\t\t\t\t\ta.systemInfo.DashboardTemp = gpu.Temperature\n\t\t\t\t\t}\n\t\t\t\t\tif gpu.Temperature > highestTemp {\n\t\t\t\t\t\thighestTemp = gpu.Temperature\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// update high gpu percent for dashboard\n\t\t\t\ta.systemInfo.GpuPct = max(a.systemInfo.GpuPct, gpu.Usage)\n\t\t\t}\n\t\t\t// use highest temp for dashboard temp if dashboard temp is unset\n\t\t\tif a.systemInfo.DashboardTemp == 0 {\n\t\t\t\ta.systemInfo.DashboardTemp = highestTemp\n\t\t\t}\n\t\t}\n\t}\n\n\t// update system info\n\ta.systemInfo.ConnectionType = a.connectionManager.ConnectionType\n\ta.systemInfo.Cpu = systemStats.Cpu\n\ta.systemInfo.LoadAvg = systemStats.LoadAvg\n\ta.systemInfo.MemPct = systemStats.MemPct\n\ta.systemInfo.DiskPct = systemStats.DiskPct\n\ta.systemInfo.Battery = systemStats.Battery\n\ta.systemInfo.Uptime, _ = host.Uptime()\n\ta.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]\n\ta.systemInfo.Threads = a.systemDetails.Threads\n\n\treturn systemStats\n}\n\n// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems\nfunc getOsPrettyName() (string, error) {\n\tfile, err := os.Open(\"/etc/os-release\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif after, ok := strings.CutPrefix(line, \"PRETTY_NAME=\"); ok {\n\t\t\tvalue := after\n\t\t\tvalue = strings.Trim(value, `\"`)\n\t\t\treturn value, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"pretty name not found\")\n}\n"
  },
  {
    "path": "agent/systemd.go",
    "content": "//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\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/go-systemd/v22/dbus\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\nvar errNoActiveTime = errors.New(\"no active time\")\n\n// systemdManager manages the collection of systemd service statistics.\ntype systemdManager struct {\n\tsync.Mutex\n\tserviceStatsMap map[string]*systemd.Service\n\tisRunning       bool\n\thasFreshStats   bool\n\tpatterns        []string\n}\n\n// isSystemdAvailable checks if systemd is used on the system to avoid unnecessary connection attempts (#1548)\nfunc isSystemdAvailable() bool {\n\tpaths := []string{\n\t\t\"/run/systemd/system\",\n\t\t\"/run/dbus/system_bus_socket\",\n\t\t\"/var/run/dbus/system_bus_socket\",\n\t}\n\tfor _, path := range paths {\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\tif data, err := os.ReadFile(\"/proc/1/comm\"); err == nil {\n\t\treturn strings.TrimSpace(string(data)) == \"systemd\"\n\t}\n\treturn false\n}\n\n// newSystemdManager creates a new systemdManager.\nfunc newSystemdManager() (*systemdManager, error) {\n\tif skipSystemd, _ := utils.GetEnv(\"SKIP_SYSTEMD\"); skipSystemd == \"true\" {\n\t\treturn nil, nil\n\t}\n\n\t// Check if systemd is available on the system before attempting connection\n\tif !isSystemdAvailable() {\n\t\tslog.Debug(\"Systemd not available\")\n\t\treturn nil, nil\n\t}\n\n\tconn, err := dbus.NewSystemConnectionContext(context.Background())\n\tif err != nil {\n\t\tslog.Debug(\"Error connecting to systemd\", \"err\", err, \"ref\", \"https://beszel.dev/guide/systemd\")\n\t\treturn nil, err\n\t}\n\n\tmanager := &systemdManager{\n\t\tserviceStatsMap: make(map[string]*systemd.Service),\n\t\tpatterns:        getServicePatterns(),\n\t}\n\n\tmanager.startWorker(conn)\n\n\treturn manager, nil\n}\n\nfunc (sm *systemdManager) startWorker(conn *dbus.Conn) {\n\tif sm.isRunning {\n\t\treturn\n\t}\n\tsm.isRunning = true\n\t// prime the service stats map with the current services\n\t_ = sm.getServiceStats(conn, true)\n\t// update the services every 10 minutes\n\tgo func() {\n\t\tfor {\n\t\t\ttime.Sleep(time.Minute * 10)\n\t\t\t_ = sm.getServiceStats(nil, true)\n\t\t}\n\t}()\n}\n\n// getServiceStatsCount returns the number of systemd services.\nfunc (sm *systemdManager) getServiceStatsCount() int {\n\treturn len(sm.serviceStatsMap)\n}\n\n// getFailedServiceCount returns the number of systemd services in a failed state.\nfunc (sm *systemdManager) getFailedServiceCount() uint16 {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\tcount := uint16(0)\n\tfor _, service := range sm.serviceStatsMap {\n\t\tif service.State == systemd.StatusFailed {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n// getServiceStats collects statistics for all running systemd services.\nfunc (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {\n\t// start := time.Now()\n\t// defer func() {\n\t// \tslog.Info(\"systemdManager.getServiceStats\", \"duration\", time.Since(start))\n\t// }()\n\n\tvar services []*systemd.Service\n\tvar err error\n\n\tif !refresh {\n\t\t// return nil\n\t\tsm.Lock()\n\t\tdefer sm.Unlock()\n\t\tfor _, service := range sm.serviceStatsMap {\n\t\t\tservices = append(services, service)\n\t\t}\n\t\tsm.hasFreshStats = false\n\t\treturn services\n\t}\n\n\tif conn == nil || !conn.Connected() {\n\t\tconn, err = dbus.NewSystemConnectionContext(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tdefer conn.Close()\n\t}\n\n\tunits, err := conn.ListUnitsByPatternsContext(context.Background(), []string{\"loaded\"}, sm.patterns)\n\tif err != nil {\n\t\tslog.Error(\"Error listing systemd service units\", \"err\", err)\n\t\treturn nil\n\t}\n\n\t// Track which units are currently present to remove stale entries\n\tcurrentUnits := make(map[string]struct{}, len(units))\n\n\tfor _, unit := range units {\n\t\tcurrentUnits[unit.Name] = struct{}{}\n\t\tservice, err := sm.updateServiceStats(conn, unit)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tservices = append(services, service)\n\t}\n\n\t// Remove services that no longer exist in systemd\n\tsm.Lock()\n\tfor unitName := range sm.serviceStatsMap {\n\t\tif _, exists := currentUnits[unitName]; !exists {\n\t\t\tdelete(sm.serviceStatsMap, unitName)\n\t\t}\n\t}\n\tsm.Unlock()\n\n\tsm.hasFreshStats = true\n\treturn services\n}\n\n// updateServiceStats updates the statistics for a single systemd service.\nfunc (sm *systemdManager) updateServiceStats(conn *dbus.Conn, unit dbus.UnitStatus) (*systemd.Service, error) {\n\tsm.Lock()\n\tdefer sm.Unlock()\n\n\tctx := context.Background()\n\n\t// if service has never been active (no active since time), skip it\n\tif activeEnterTsProp, err := conn.GetUnitTypePropertyContext(ctx, unit.Name, \"Unit\", \"ActiveEnterTimestamp\"); err == nil {\n\t\tif ts, ok := activeEnterTsProp.Value.Value().(uint64); !ok || ts == 0 || ts == math.MaxUint64 {\n\t\t\treturn nil, errNoActiveTime\n\t\t}\n\t} else {\n\t\treturn nil, err\n\t}\n\n\tservice, serviceExists := sm.serviceStatsMap[unit.Name]\n\tif !serviceExists {\n\t\tservice = &systemd.Service{Name: unescapeServiceName(strings.TrimSuffix(unit.Name, \".service\"))}\n\t\tsm.serviceStatsMap[unit.Name] = service\n\t}\n\n\tmemPeak := service.MemPeak\n\tif memPeakProp, err := conn.GetUnitTypePropertyContext(ctx, unit.Name, \"Service\", \"MemoryPeak\"); err == nil {\n\t\t// If memPeak is MaxUint64 the api is saying it's not available\n\t\tif v, ok := memPeakProp.Value.Value().(uint64); ok && v != math.MaxUint64 {\n\t\t\tmemPeak = v\n\t\t}\n\t}\n\n\tvar memUsage uint64\n\tif memProp, err := conn.GetUnitTypePropertyContext(ctx, unit.Name, \"Service\", \"MemoryCurrent\"); err == nil {\n\t\t// If memUsage is MaxUint64 the api is saying it's not available\n\t\tif v, ok := memProp.Value.Value().(uint64); ok && v != math.MaxUint64 {\n\t\t\tmemUsage = v\n\t\t}\n\t}\n\n\tservice.State = systemd.ParseServiceStatus(unit.ActiveState)\n\tservice.Sub = systemd.ParseServiceSubState(unit.SubState)\n\n\t// some systems always return 0 for mem peak, so we should update the peak if the current usage is greater\n\tif memUsage > memPeak {\n\t\tmemPeak = memUsage\n\t}\n\n\tvar cpuUsage uint64\n\tif cpuProp, err := conn.GetUnitTypePropertyContext(ctx, unit.Name, \"Service\", \"CPUUsageNSec\"); err == nil {\n\t\tif v, ok := cpuProp.Value.Value().(uint64); ok {\n\t\t\tcpuUsage = v\n\t\t}\n\t}\n\n\tservice.Mem = memUsage\n\tif memPeak > service.MemPeak {\n\t\tservice.MemPeak = memPeak\n\t}\n\tservice.UpdateCPUPercent(cpuUsage)\n\n\treturn service, nil\n}\n\n// getServiceDetails collects extended information for a specific systemd service.\nfunc (sm *systemdManager) getServiceDetails(serviceName string) (systemd.ServiceDetails, error) {\n\tconn, err := dbus.NewSystemConnectionContext(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer conn.Close()\n\n\tunitName := serviceName\n\tif !strings.HasSuffix(unitName, \".service\") {\n\t\tunitName += \".service\"\n\t}\n\n\tctx := context.Background()\n\tprops, err := conn.GetUnitPropertiesContext(ctx, unitName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Start with all unit properties\n\tdetails := make(systemd.ServiceDetails)\n\tmaps.Copy(details, props)\n\n\t// // Add service-specific properties\n\tservicePropNames := []string{\n\t\t\"MainPID\", \"ExecMainPID\", \"TasksCurrent\", \"TasksMax\",\n\t\t\"MemoryCurrent\", \"MemoryPeak\", \"MemoryLimit\", \"CPUUsageNSec\",\n\t\t\"NRestarts\", \"ExecMainStartTimestampRealtime\", \"Result\",\n\t}\n\n\tfor _, propName := range servicePropNames {\n\t\tif variant, err := conn.GetUnitTypePropertyContext(ctx, unitName, \"Service\", propName); err == nil {\n\t\t\tvalue := variant.Value.Value()\n\t\t\t// Check if the value is MaxUint64, which indicates unlimited/infinite\n\t\t\tif uint64Value, ok := value.(uint64); ok && uint64Value == math.MaxUint64 {\n\t\t\t\t// Set to nil to indicate unlimited - frontend will handle this appropriately\n\t\t\t\tdetails[propName] = nil\n\t\t\t} else {\n\t\t\t\tdetails[propName] = value\n\t\t\t}\n\t\t}\n\t}\n\n\treturn details, nil\n}\n\n// unescapeServiceName unescapes systemd service names that contain C-style escape sequences like \\x2d\nfunc unescapeServiceName(name string) string {\n\tif !strings.Contains(name, \"\\\\x\") {\n\t\treturn name\n\t}\n\tunescaped, err := strconv.Unquote(\"\\\"\" + name + \"\\\"\")\n\tif err != nil {\n\t\treturn name\n\t}\n\treturn unescaped\n}\n\n// getServicePatterns returns the list of service patterns to match.\n// It reads from the SERVICE_PATTERNS environment variable if set,\n// otherwise defaults to \"*service\".\nfunc getServicePatterns() []string {\n\tpatterns := []string{}\n\tif envPatterns, _ := utils.GetEnv(\"SERVICE_PATTERNS\"); envPatterns != \"\" {\n\t\tfor pattern := range strings.SplitSeq(envPatterns, \",\") {\n\t\t\tpattern = strings.TrimSpace(pattern)\n\t\t\tif pattern == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !strings.HasSuffix(pattern, \"timer\") && !strings.HasSuffix(pattern, \".service\") {\n\t\t\t\tpattern += \".service\"\n\t\t\t}\n\t\t\tpatterns = append(patterns, pattern)\n\t\t}\n\t}\n\tif len(patterns) == 0 {\n\t\tpatterns = []string{\"*.service\"}\n\t}\n\treturn patterns\n}\n"
  },
  {
    "path": "agent/systemd_nonlinux.go",
    "content": "//go:build !linux\n\npackage agent\n\nimport (\n\t\"errors\"\n\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\n// systemdManager manages the collection of systemd service statistics.\ntype systemdManager struct {\n\thasFreshStats bool\n}\n\n// newSystemdManager creates a new systemdManager.\nfunc newSystemdManager() (*systemdManager, error) {\n\treturn &systemdManager{}, nil\n}\n\n// getServiceStats returns nil for non-linux systems.\nfunc (sm *systemdManager) getServiceStats(conn any, refresh bool) []*systemd.Service {\n\treturn nil\n}\n\n// getServiceStatsCount returns 0 for non-linux systems.\nfunc (sm *systemdManager) getServiceStatsCount() int {\n\treturn 0\n}\n\n// getFailedServiceCount returns 0 for non-linux systems.\nfunc (sm *systemdManager) getFailedServiceCount() uint16 {\n\treturn 0\n}\n\nfunc (sm *systemdManager) getServiceDetails(string) (systemd.ServiceDetails, error) {\n\treturn nil, errors.New(\"systemd manager unavailable\")\n}\n"
  },
  {
    "path": "agent/systemd_nonlinux_test.go",
    "content": "//go:build !linux && testing\n\npackage agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewSystemdManager(t *testing.T) {\n\tmanager, err := newSystemdManager()\n\tassert.NoError(t, err)\n\tassert.NotNil(t, manager)\n}\n\nfunc TestSystemdManagerGetServiceStats(t *testing.T) {\n\tmanager, err := newSystemdManager()\n\tassert.NoError(t, err)\n\n\t// Test with refresh = true\n\tresult := manager.getServiceStats(\"any-service\", true)\n\tassert.Nil(t, result)\n\n\t// Test with refresh = false\n\tresult = manager.getServiceStats(\"any-service\", false)\n\tassert.Nil(t, result)\n}\n\nfunc TestSystemdManagerGetServiceDetails(t *testing.T) {\n\tmanager, err := newSystemdManager()\n\tassert.NoError(t, err)\n\n\tresult, err := manager.getServiceDetails(\"any-service\")\n\tassert.Error(t, err)\n\tassert.Equal(t, \"systemd manager unavailable\", err.Error())\n\tassert.Nil(t, result)\n\n\t// Test with empty service name\n\tresult, err = manager.getServiceDetails(\"\")\n\tassert.Error(t, err)\n\tassert.Equal(t, \"systemd manager unavailable\", err.Error())\n\tassert.Nil(t, result)\n}\n\nfunc TestSystemdManagerFields(t *testing.T) {\n\tmanager, err := newSystemdManager()\n\tassert.NoError(t, err)\n\n\t// The non-linux manager should be a simple struct with no special fields\n\t// We can't test private fields directly, but we can test the methods work\n\tassert.NotNil(t, manager)\n}\n"
  },
  {
    "path": "agent/systemd_test.go",
    "content": "//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)\n\nfunc TestUnescapeServiceName(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"nginx.service\", \"nginx.service\"},                                     // No escaping needed\n\t\t{\"test\\\\x2dwith\\\\x2ddashes.service\", \"test-with-dashes.service\"},       // \\x2d is dash\n\t\t{\"service\\\\x20with\\\\x20spaces.service\", \"service with spaces.service\"}, // \\x20 is space\n\t\t{\"mixed\\\\x2dand\\\\x2dnormal\", \"mixed-and-normal\"},                       // Mixed escaped and normal\n\t\t{\"no-escape-here\", \"no-escape-here\"},                                   // No escape sequences\n\t\t{\"\", \"\"},                                                               // Empty string\n\t\t{\"\\\\x2d\\\\x2d\", \"--\"},                                                   // Multiple escapes\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.input, func(t *testing.T) {\n\t\t\tresult := unescapeServiceName(test.input)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestUnescapeServiceNameInvalid(t *testing.T) {\n\t// Test invalid escape sequences - should return original string\n\tinvalidInputs := []string{\n\t\t\"invalid\\\\x\",   // Incomplete escape\n\t\t\"invalid\\\\xZZ\", // Invalid hex\n\t\t\"invalid\\\\x2\",  // Incomplete hex\n\t\t\"invalid\\\\xyz\", // Not a valid escape\n\t}\n\n\tfor _, input := range invalidInputs {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\tresult := unescapeServiceName(input)\n\t\t\tassert.Equal(t, input, result, \"Invalid escape sequences should return original string\")\n\t\t})\n\t}\n}\n\nfunc TestIsSystemdAvailable(t *testing.T) {\n\t// Note: This test's result will vary based on the actual system running the tests\n\t// On systems with systemd, it should return true\n\t// On systems without systemd, it should return false\n\tresult := isSystemdAvailable()\n\n\t// Check if either the /run/systemd/system directory exists or PID 1 is systemd\n\trunSystemdExists := false\n\tif _, err := os.Stat(\"/run/systemd/system\"); err == nil {\n\t\trunSystemdExists = true\n\t}\n\n\tpid1IsSystemd := false\n\tif data, err := os.ReadFile(\"/proc/1/comm\"); err == nil {\n\t\tpid1IsSystemd = strings.TrimSpace(string(data)) == \"systemd\"\n\t}\n\n\texpected := runSystemdExists || pid1IsSystemd\n\n\tassert.Equal(t, expected, result, \"isSystemdAvailable should correctly detect systemd presence\")\n\n\t// Log the result for informational purposes\n\tif result {\n\t\tt.Log(\"Systemd is available on this system\")\n\t} else {\n\t\tt.Log(\"Systemd is not available on this system\")\n\t}\n}\n\nfunc TestGetServicePatterns(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tprefixedEnv    string\n\t\tunprefixedEnv  string\n\t\texpected       []string\n\t\tcleanupEnvVars bool\n\t}{\n\t\t{\n\t\t\tname:           \"default when no env var set\",\n\t\t\tprefixedEnv:    \"\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"*.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"single pattern with prefixed env\",\n\t\t\tprefixedEnv:    \"nginx\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"single pattern with unprefixed env\",\n\t\t\tprefixedEnv:    \"\",\n\t\t\tunprefixedEnv:  \"nginx\",\n\t\t\texpected:       []string{\"nginx.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"prefixed env takes precedence\",\n\t\t\tprefixedEnv:    \"nginx\",\n\t\t\tunprefixedEnv:  \"apache\",\n\t\t\texpected:       []string{\"nginx.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple patterns\",\n\t\t\tprefixedEnv:    \"nginx,apache,postgresql\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"apache.service\", \"postgresql.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"patterns with .service suffix\",\n\t\t\tprefixedEnv:    \"nginx.service,apache.service\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"apache.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"mixed patterns with and without suffix\",\n\t\t\tprefixedEnv:    \"nginx.service,apache,postgresql.service\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"apache.service\", \"postgresql.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"patterns with whitespace\",\n\t\t\tprefixedEnv:    \" nginx , apache , postgresql \",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"apache.service\", \"postgresql.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"empty patterns are skipped\",\n\t\t\tprefixedEnv:    \"nginx,,apache,  ,postgresql\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"apache.service\", \"postgresql.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"wildcard pattern\",\n\t\t\tprefixedEnv:    \"*nginx*,*apache*\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"*nginx*.service\", \"*apache*.service\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"opt into timer monitoring\",\n\t\t\tprefixedEnv:    \"nginx.service,docker,apache.timer\",\n\t\t\tunprefixedEnv:  \"\",\n\t\t\texpected:       []string{\"nginx.service\", \"docker.service\", \"apache.timer\"},\n\t\t\tcleanupEnvVars: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Clean up any existing env vars\n\t\t\tos.Unsetenv(\"BESZEL_AGENT_SERVICE_PATTERNS\")\n\t\t\tos.Unsetenv(\"SERVICE_PATTERNS\")\n\n\t\t\t// Set up environment variables\n\t\t\tif tt.prefixedEnv != \"\" {\n\t\t\t\tos.Setenv(\"BESZEL_AGENT_SERVICE_PATTERNS\", tt.prefixedEnv)\n\t\t\t}\n\t\t\tif tt.unprefixedEnv != \"\" {\n\t\t\t\tos.Setenv(\"SERVICE_PATTERNS\", tt.unprefixedEnv)\n\t\t\t}\n\n\t\t\t// Run the function\n\t\t\tresult := getServicePatterns()\n\n\t\t\t// Verify results\n\t\t\tassert.Equal(t, tt.expected, result, \"Patterns should match expected values\")\n\n\t\t\t// Cleanup\n\t\t\tif tt.cleanupEnvVars {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_SERVICE_PATTERNS\")\n\t\t\t\tos.Unsetenv(\"SERVICE_PATTERNS\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "agent/test-data/amdgpu.ids",
    "content": "# List of AMDGPU IDs\n#\n# Syntax:\n# device_id,\trevision_id,\tproduct_name        <-- single tab after comma\n\n1.0.0\n1114,\tC2,\tAMD Radeon 860M Graphics\n1114,\tC3,\tAMD Radeon 840M Graphics\n1114,\tD2,\tAMD Radeon 860M Graphics\n1114,\tD3,\tAMD Radeon 840M Graphics\n1309,\t00,\tAMD Radeon R7 Graphics\n130A,\t00,\tAMD Radeon R6 Graphics\n130B,\t00,\tAMD Radeon R4 Graphics\n130C,\t00,\tAMD Radeon R7 Graphics\n130D,\t00,\tAMD Radeon R6 Graphics\n130E,\t00,\tAMD Radeon R5 Graphics\n130F,\t00,\tAMD Radeon R7 Graphics\n130F,\tD4,\tAMD Radeon R7 Graphics\n130F,\tD5,\tAMD Radeon R7 Graphics\n130F,\tD6,\tAMD Radeon R7 Graphics\n130F,\tD7,\tAMD Radeon R7 Graphics\n1313,\t00,\tAMD Radeon R7 Graphics\n1313,\tD4,\tAMD Radeon R7 Graphics\n1313,\tD5,\tAMD Radeon R7 Graphics\n1313,\tD6,\tAMD Radeon R7 Graphics\n1315,\t00,\tAMD Radeon R5 Graphics\n1315,\tD4,\tAMD Radeon R5 Graphics\n1315,\tD5,\tAMD Radeon R5 Graphics\n1315,\tD6,\tAMD Radeon R5 Graphics\n1315,\tD7,\tAMD Radeon R5 Graphics\n1316,\t00,\tAMD Radeon R5 Graphics\n1318,\t00,\tAMD Radeon R5 Graphics\n131B,\t00,\tAMD Radeon R4 Graphics\n131C,\t00,\tAMD Radeon R7 Graphics\n131D,\t00,\tAMD Radeon R6 Graphics\n1435,\tAE,\tAMD Custom GPU 0932\n1506,\tC1,\tAMD Radeon 610M\n1506,\tC2,\tAMD Radeon 610M\n1506,\tC3,\tAMD Radeon 610M\n1506,\tC4,\tAMD Radeon 610M\n150E,\tC1,\tAMD Radeon 890M Graphics\n150E,\tC4,\tAMD Radeon 890M Graphics\n150E,\tC5,\tAMD Radeon 890M Graphics\n150E,\tC6,\tAMD Radeon 890M Graphics\n150E,\tD1,\tAMD Radeon 890M Graphics\n150E,\tD2,\tAMD Radeon 890M Graphics\n150E,\tD3,\tAMD Radeon 890M Graphics\n1586,\tC1,\tRadeon 8060S Graphics\n1586,\tC2,\tRadeon 8050S Graphics\n1586,\tC4,\tRadeon 8050S Graphics\n1586,\tD1,\tRadeon 8060S Graphics\n1586,\tD2,\tRadeon 8050S Graphics\n1586,\tD4,\tRadeon 8050S Graphics\n1586,\tD5,\tRadeon 8040S Graphics\n15BF,\t00,\tAMD Radeon 780M Graphics\n15BF,\t01,\tAMD Radeon 760M Graphics\n15BF,\t02,\tAMD Radeon 780M Graphics\n15BF,\t03,\tAMD Radeon 760M Graphics\n15BF,\tC1,\tAMD Radeon 780M Graphics\n15BF,\tC2,\tAMD Radeon 780M Graphics\n15BF,\tC3,\tAMD Radeon 760M Graphics\n15BF,\tC4,\tAMD Radeon 780M Graphics\n15BF,\tC5,\tAMD Radeon 740M Graphics\n15BF,\tC6,\tAMD Radeon 780M Graphics\n15BF,\tC7,\tAMD Radeon 780M Graphics\n15BF,\tC8,\tAMD Radeon 760M Graphics\n15BF,\tC9,\tAMD Radeon 780M Graphics\n15BF,\tCA,\tAMD Radeon 740M Graphics\n15BF,\tCB,\tAMD Radeon 760M Graphics\n15BF,\tCC,\tAMD Radeon 740M Graphics\n15BF,\tCD,\tAMD Radeon 760M Graphics\n15BF,\tCF,\tAMD Radeon 780M Graphics\n15BF,\tD0,\tAMD Radeon 780M Graphics\n15BF,\tD1,\tAMD Radeon 780M Graphics\n15BF,\tD2,\tAMD Radeon 780M Graphics\n15BF,\tD3,\tAMD Radeon 780M Graphics\n15BF,\tD4,\tAMD Radeon 780M Graphics\n15BF,\tD5,\tAMD Radeon 760M Graphics\n15BF,\tD6,\tAMD Radeon 760M Graphics\n15BF,\tD7,\tAMD Radeon 780M Graphics\n15BF,\tD8,\tAMD Radeon 740M Graphics\n15BF,\tD9,\tAMD Radeon 780M Graphics\n15BF,\tDA,\tAMD Radeon 780M Graphics\n15BF,\tDB,\tAMD Radeon 760M Graphics\n15BF,\tDC,\tAMD Radeon 760M Graphics\n15BF,\tDD,\tAMD Radeon 780M Graphics\n15BF,\tDE,\tAMD Radeon 740M Graphics\n15BF,\tDF,\tAMD Radeon 760M Graphics\n15BF,\tF0,\tAMD Radeon 760M Graphics\n15C8,\tC1,\tAMD Radeon 740M Graphics\n15C8,\tC2,\tAMD Radeon 740M Graphics\n15C8,\tC3,\tAMD Radeon 740M Graphics\n15C8,\tC4,\tAMD Radeon 740M Graphics\n15C8,\tD1,\tAMD Radeon 740M Graphics\n15C8,\tD2,\tAMD Radeon 740M Graphics\n15C8,\tD3,\tAMD Radeon 740M Graphics\n15C8,\tD4,\tAMD Radeon 740M Graphics\n15D8,\t00,\tAMD Radeon RX Vega 8 Graphics WS\n15D8,\t91,\tAMD Radeon Vega 3 Graphics\n15D8,\t91,\tAMD Ryzen Embedded R1606G with Radeon Vega Gfx\n15D8,\t92,\tAMD Radeon Vega 3 Graphics\n15D8,\t92,\tAMD Ryzen Embedded R1505G with Radeon Vega Gfx\n15D8,\t93,\tAMD Radeon Vega 1 Graphics\n15D8,\tA1,\tAMD Radeon Vega 10 Graphics\n15D8,\tA2,\tAMD Radeon Vega 8 Graphics\n15D8,\tA3,\tAMD Radeon Vega 6 Graphics\n15D8,\tA4,\tAMD Radeon Vega 3 Graphics\n15D8,\tB1,\tAMD Radeon Vega 10 Graphics\n15D8,\tB2,\tAMD Radeon Vega 8 Graphics\n15D8,\tB3,\tAMD Radeon Vega 6 Graphics\n15D8,\tB4,\tAMD Radeon Vega 3 Graphics\n15D8,\tC1,\tAMD Radeon Vega 10 Graphics\n15D8,\tC2,\tAMD Radeon Vega 8 Graphics\n15D8,\tC3,\tAMD Radeon Vega 6 Graphics\n15D8,\tC4,\tAMD Radeon Vega 3 Graphics\n15D8,\tC5,\tAMD Radeon Vega 3 Graphics\n15D8,\tC8,\tAMD Radeon Vega 11 Graphics\n15D8,\tC9,\tAMD Radeon Vega 8 Graphics\n15D8,\tCA,\tAMD Radeon Vega 11 Graphics\n15D8,\tCB,\tAMD Radeon Vega 8 Graphics\n15D8,\tCC,\tAMD Radeon Vega 3 Graphics\n15D8,\tCE,\tAMD Radeon Vega 3 Graphics\n15D8,\tCF,\tAMD Ryzen Embedded R1305G with Radeon Vega Gfx\n15D8,\tD1,\tAMD Radeon Vega 10 Graphics\n15D8,\tD2,\tAMD Radeon Vega 8 Graphics\n15D8,\tD3,\tAMD Radeon Vega 6 Graphics\n15D8,\tD4,\tAMD Radeon Vega 3 Graphics\n15D8,\tD8,\tAMD Radeon Vega 11 Graphics\n15D8,\tD9,\tAMD Radeon Vega 8 Graphics\n15D8,\tDA,\tAMD Radeon Vega 11 Graphics\n15D8,\tDB,\tAMD Radeon Vega 3 Graphics\n15D8,\tDB,\tAMD Radeon Vega 8 Graphics\n15D8,\tDC,\tAMD Radeon Vega 3 Graphics\n15D8,\tDD,\tAMD Radeon Vega 3 Graphics\n15D8,\tDE,\tAMD Radeon Vega 3 Graphics\n15D8,\tDF,\tAMD Radeon Vega 3 Graphics\n15D8,\tE3,\tAMD Radeon Vega 3 Graphics\n15D8,\tE4,\tAMD Ryzen Embedded R1102G with Radeon Vega Gfx\n15DD,\t81,\tAMD Ryzen Embedded V1807B with Radeon Vega Gfx\n15DD,\t82,\tAMD Ryzen Embedded V1756B with Radeon Vega Gfx\n15DD,\t83,\tAMD Ryzen Embedded V1605B with Radeon Vega Gfx\n15DD,\t84,\tAMD Radeon Vega 6 Graphics\n15DD,\t85,\tAMD Ryzen Embedded V1202B with Radeon Vega Gfx\n15DD,\t86,\tAMD Radeon Vega 11 Graphics\n15DD,\t88,\tAMD Radeon Vega 8 Graphics\n15DD,\tC1,\tAMD Radeon Vega 11 Graphics\n15DD,\tC2,\tAMD Radeon Vega 8 Graphics\n15DD,\tC3,\tAMD Radeon Vega 3 / 10 Graphics\n15DD,\tC4,\tAMD Radeon Vega 8 Graphics\n15DD,\tC5,\tAMD Radeon Vega 3 Graphics\n15DD,\tC6,\tAMD Radeon Vega 11 Graphics\n15DD,\tC8,\tAMD Radeon Vega 8 Graphics\n15DD,\tC9,\tAMD Radeon Vega 11 Graphics\n15DD,\tCA,\tAMD Radeon Vega 8 Graphics\n15DD,\tCB,\tAMD Radeon Vega 3 Graphics\n15DD,\tCC,\tAMD Radeon Vega 6 Graphics\n15DD,\tCE,\tAMD Radeon Vega 3 Graphics\n15DD,\tCF,\tAMD Radeon Vega 3 Graphics\n15DD,\tD0,\tAMD Radeon Vega 10 Graphics\n15DD,\tD1,\tAMD Radeon Vega 8 Graphics\n15DD,\tD3,\tAMD Radeon Vega 11 Graphics\n15DD,\tD5,\tAMD Radeon Vega 8 Graphics\n15DD,\tD6,\tAMD Radeon Vega 11 Graphics\n15DD,\tD7,\tAMD Radeon Vega 8 Graphics\n15DD,\tD8,\tAMD Radeon Vega 3 Graphics\n15DD,\tD9,\tAMD Radeon Vega 6 Graphics\n15DD,\tE1,\tAMD Radeon Vega 3 Graphics\n15DD,\tE2,\tAMD Radeon Vega 3 Graphics\n163F,\tAE,\tAMD Custom GPU 0405\n163F,\tE1,\tAMD Custom GPU 0405\n164E,\tD8,\tAMD Radeon 610M\n164E,\tD9,\tAMD Radeon 610M\n164E,\tDA,\tAMD Radeon 610M\n164E,\tDB,\tAMD Radeon 610M\n164E,\tDC,\tAMD Radeon 610M\n1681,\t06,\tAMD Radeon 680M\n1681,\t07,\tAMD Radeon 660M\n1681,\t0A,\tAMD Radeon 680M\n1681,\t0B,\tAMD Radeon 660M\n1681,\tC7,\tAMD Radeon 680M\n1681,\tC8,\tAMD Radeon 680M\n1681,\tC9,\tAMD Radeon 660M\n1900,\t01,\tAMD Radeon 780M Graphics\n1900,\t02,\tAMD Radeon 760M Graphics\n1900,\t03,\tAMD Radeon 780M Graphics\n1900,\t04,\tAMD Radeon 760M Graphics\n1900,\t05,\tAMD Radeon 780M Graphics\n1900,\t06,\tAMD Radeon 780M Graphics\n1900,\t07,\tAMD Radeon 760M Graphics\n1900,\tB0,\tAMD Radeon 780M Graphics\n1900,\tB1,\tAMD Radeon 780M Graphics\n1900,\tB2,\tAMD Radeon 780M Graphics\n1900,\tB3,\tAMD Radeon 780M Graphics\n1900,\tB4,\tAMD Radeon 780M Graphics\n1900,\tB5,\tAMD Radeon 780M Graphics\n1900,\tB6,\tAMD Radeon 780M Graphics\n1900,\tB7,\tAMD Radeon 760M Graphics\n1900,\tB8,\tAMD Radeon 760M Graphics\n1900,\tB9,\tAMD Radeon 780M Graphics\n1900,\tBA,\tAMD Radeon 780M Graphics\n1900,\tBB,\tAMD Radeon 780M Graphics\n1900,\tC0,\tAMD Radeon 780M Graphics\n1900,\tC1,\tAMD Radeon 760M Graphics\n1900,\tC2,\tAMD Radeon 780M Graphics\n1900,\tC3,\tAMD Radeon 760M Graphics\n1900,\tC4,\tAMD Radeon 780M Graphics\n1900,\tC5,\tAMD Radeon 780M Graphics\n1900,\tC6,\tAMD Radeon 760M Graphics\n1900,\tC7,\tAMD Radeon 780M Graphics\n1900,\tC8,\tAMD Radeon 760M Graphics\n1900,\tC9,\tAMD Radeon 780M Graphics\n1900,\tCA,\tAMD Radeon 760M Graphics\n1900,\tCB,\tAMD Radeon 780M Graphics\n1900,\tCC,\tAMD Radeon 780M Graphics\n1900,\tCD,\tAMD Radeon 760M Graphics\n1900,\tCE,\tAMD Radeon 780M Graphics\n1900,\tCF,\tAMD Radeon 760M Graphics\n1900,\tD0,\tAMD Radeon 780M Graphics\n1900,\tD1,\tAMD Radeon 760M Graphics\n1900,\tD2,\tAMD Radeon 780M Graphics\n1900,\tD3,\tAMD Radeon 760M Graphics\n1900,\tD4,\tAMD Radeon 780M Graphics\n1900,\tD5,\tAMD Radeon 780M Graphics\n1900,\tD6,\tAMD Radeon 760M Graphics\n1900,\tD7,\tAMD Radeon 780M Graphics\n1900,\tD8,\tAMD Radeon 760M Graphics\n1900,\tD9,\tAMD Radeon 780M Graphics\n1900,\tDA,\tAMD Radeon 760M Graphics\n1900,\tDB,\tAMD Radeon 780M Graphics\n1900,\tDC,\tAMD Radeon 780M Graphics\n1900,\tDD,\tAMD Radeon 760M Graphics\n1900,\tDE,\tAMD Radeon 780M Graphics\n1900,\tDF,\tAMD Radeon 760M Graphics\n1900,\tF0,\tAMD Radeon 780M Graphics\n1900,\tF1,\tAMD Radeon 780M Graphics\n1900,\tF2,\tAMD Radeon 780M Graphics\n1901,\tC1,\tAMD Radeon 740M Graphics\n1901,\tC2,\tAMD Radeon 740M Graphics\n1901,\tC3,\tAMD Radeon 740M Graphics\n1901,\tC6,\tAMD Radeon 740M Graphics\n1901,\tC7,\tAMD Radeon 740M Graphics\n1901,\tC8,\tAMD Radeon 740M Graphics\n1901,\tC9,\tAMD Radeon 740M Graphics\n1901,\tCA,\tAMD Radeon 740M Graphics\n1901,\tD1,\tAMD Radeon 740M Graphics\n1901,\tD2,\tAMD Radeon 740M Graphics\n1901,\tD3,\tAMD Radeon 740M Graphics\n1901,\tD4,\tAMD Radeon 740M Graphics\n1901,\tD5,\tAMD Radeon 740M Graphics\n1901,\tD6,\tAMD Radeon 740M Graphics\n1901,\tD7,\tAMD Radeon 740M Graphics\n1901,\tD8,\tAMD Radeon 740M Graphics\n6600,\t00,\tAMD Radeon HD 8600 / 8700M\n6600,\t81,\tAMD Radeon R7 M370\n6601,\t00,\tAMD Radeon HD 8500M / 8700M\n6604,\t00,\tAMD Radeon R7 M265 Series\n6604,\t81,\tAMD Radeon R7 M350\n6605,\t00,\tAMD Radeon R7 M260 Series\n6605,\t81,\tAMD Radeon R7 M340\n6606,\t00,\tAMD Radeon HD 8790M\n6607,\t00,\tAMD Radeon R5 M240\n6608,\t00,\tAMD FirePro W2100\n6610,\t00,\tAMD Radeon R7 200 Series\n6610,\t81,\tAMD Radeon R7 350\n6610,\t83,\tAMD Radeon R5 340\n6610,\t87,\tAMD Radeon R7 200 Series\n6611,\t00,\tAMD Radeon R7 200 Series\n6611,\t87,\tAMD Radeon R7 200 Series\n6613,\t00,\tAMD Radeon R7 200 Series\n6617,\t00,\tAMD Radeon R7 240 Series\n6617,\t87,\tAMD Radeon R7 200 Series\n6617,\tC7,\tAMD Radeon R7 240 Series\n6640,\t00,\tAMD Radeon HD 8950\n6640,\t80,\tAMD Radeon R9 M380\n6646,\t00,\tAMD Radeon R9 M280X\n6646,\t80,\tAMD Radeon R9 M385\n6646,\t80,\tAMD Radeon R9 M470X\n6647,\t00,\tAMD Radeon R9 M200X Series\n6647,\t80,\tAMD Radeon R9 M380\n6649,\t00,\tAMD FirePro W5100\n6658,\t00,\tAMD Radeon R7 200 Series\n665C,\t00,\tAMD Radeon HD 7700 Series\n665D,\t00,\tAMD Radeon R7 200 Series\n665F,\t81,\tAMD Radeon R7 360 Series\n6660,\t00,\tAMD Radeon HD 8600M Series\n6660,\t81,\tAMD Radeon R5 M335\n6660,\t83,\tAMD Radeon R5 M330\n6663,\t00,\tAMD Radeon HD 8500M Series\n6663,\t83,\tAMD Radeon R5 M320\n6664,\t00,\tAMD Radeon R5 M200 Series\n6665,\t00,\tAMD Radeon R5 M230 Series\n6665,\t83,\tAMD Radeon R5 M320\n6665,\tC3,\tAMD Radeon R5 M435\n6666,\t00,\tAMD Radeon R5 M200 Series\n6667,\t00,\tAMD Radeon R5 M200 Series\n666F,\t00,\tAMD Radeon HD 8500M\n66A1,\t02,\tAMD Instinct MI60 / MI50\n66A1,\t06,\tAMD Radeon Pro VII\n66AF,\tC1,\tAMD Radeon VII\n6780,\t00,\tAMD FirePro W9000\n6784,\t00,\tATI FirePro V (FireGL V) Graphics Adapter\n6788,\t00,\tATI FirePro V (FireGL V) Graphics Adapter\n678A,\t00,\tAMD FirePro W8000\n6798,\t00,\tAMD Radeon R9 200 / HD 7900 Series\n6799,\t00,\tAMD Radeon HD 7900 Series\n679A,\t00,\tAMD Radeon HD 7900 Series\n679B,\t00,\tAMD Radeon HD 7900 Series\n679E,\t00,\tAMD Radeon HD 7800 Series\n67A0,\t00,\tAMD Radeon FirePro W9100\n67A1,\t00,\tAMD Radeon FirePro W8100\n67B0,\t00,\tAMD Radeon R9 200 Series\n67B0,\t80,\tAMD Radeon R9 390 Series\n67B1,\t00,\tAMD Radeon R9 200 Series\n67B1,\t80,\tAMD Radeon R9 390 Series\n67B9,\t00,\tAMD Radeon R9 200 Series\n67C0,\t00,\tAMD Radeon Pro WX 7100 Graphics\n67C0,\t80,\tAMD Radeon E9550\n67C2,\t01,\tAMD Radeon Pro V7350x2\n67C2,\t02,\tAMD Radeon Pro V7300X\n67C4,\t00,\tAMD Radeon Pro WX 7100 Graphics\n67C4,\t80,\tAMD Radeon E9560 / E9565 Graphics\n67C7,\t00,\tAMD Radeon Pro WX 5100 Graphics\n67C7,\t80,\tAMD Radeon E9390 Graphics\n67D0,\t01,\tAMD Radeon Pro V7350x2\n67D0,\t02,\tAMD Radeon Pro V7300X\n67DF,\tC0,\tAMD Radeon Pro 580X\n67DF,\tC1,\tAMD Radeon RX 580 Series\n67DF,\tC2,\tAMD Radeon RX 570 Series\n67DF,\tC3,\tAMD Radeon RX 580 Series\n67DF,\tC4,\tAMD Radeon RX 480 Graphics\n67DF,\tC5,\tAMD Radeon RX 470 Graphics\n67DF,\tC6,\tAMD Radeon RX 570 Series\n67DF,\tC7,\tAMD Radeon RX 480 Graphics\n67DF,\tCF,\tAMD Radeon RX 470 Graphics\n67DF,\tD7,\tAMD Radeon RX 470 Graphics\n67DF,\tE0,\tAMD Radeon RX 470 Series\n67DF,\tE1,\tAMD Radeon RX 590 Series\n67DF,\tE3,\tAMD Radeon RX Series\n67DF,\tE7,\tAMD Radeon RX 580 Series\n67DF,\tEB,\tAMD Radeon Pro 580X\n67DF,\tEF,\tAMD Radeon RX 570 Series\n67DF,\tF7,\tAMD Radeon RX P30PH\n67DF,\tFF,\tAMD Radeon RX 470 Series\n67E0,\t00,\tAMD Radeon Pro WX Series\n67E3,\t00,\tAMD Radeon Pro WX 4100\n67E8,\t00,\tAMD Radeon Pro WX Series\n67E8,\t01,\tAMD Radeon Pro WX Series\n67E8,\t80,\tAMD Radeon E9260 Graphics\n67EB,\t00,\tAMD Radeon Pro V5300X\n67EF,\tC0,\tAMD Radeon RX Graphics\n67EF,\tC1,\tAMD Radeon RX 460 Graphics\n67EF,\tC2,\tAMD Radeon Pro Series\n67EF,\tC3,\tAMD Radeon RX Series\n67EF,\tC5,\tAMD Radeon RX 460 Graphics\n67EF,\tC7,\tAMD Radeon RX Graphics\n67EF,\tCF,\tAMD Radeon RX 460 Graphics\n67EF,\tE0,\tAMD Radeon RX 560 Series\n67EF,\tE1,\tAMD Radeon RX Series\n67EF,\tE2,\tAMD Radeon RX 560X\n67EF,\tE3,\tAMD Radeon RX Series\n67EF,\tE5,\tAMD Radeon RX 560 Series\n67EF,\tE7,\tAMD Radeon RX 560 Series\n67EF,\tEF,\tAMD Radeon 550 Series\n67EF,\tFF,\tAMD Radeon RX 460 Graphics\n67FF,\tC0,\tAMD Radeon Pro 465\n67FF,\tC1,\tAMD Radeon RX 560 Series\n67FF,\tCF,\tAMD Radeon RX 560 Series\n67FF,\tEF,\tAMD Radeon RX 560 Series\n67FF,\tFF,\tAMD Radeon RX 550 Series\n6800,\t00,\tAMD Radeon HD 7970M\n6801,\t00,\tAMD Radeon HD 8970M\n6806,\t00,\tAMD Radeon R9 M290X\n6808,\t00,\tAMD FirePro W7000\n6808,\t00,\tATI FirePro V (FireGL V) Graphics Adapter\n6809,\t00,\tATI FirePro W5000\n6810,\t00,\tAMD Radeon R9 200 Series\n6810,\t81,\tAMD Radeon R9 370 Series\n6811,\t00,\tAMD Radeon R9 200 Series\n6811,\t81,\tAMD Radeon R7 370 Series\n6818,\t00,\tAMD Radeon HD 7800 Series\n6819,\t00,\tAMD Radeon HD 7800 Series\n6820,\t00,\tAMD Radeon R9 M275X\n6820,\t81,\tAMD Radeon R9 M375\n6820,\t83,\tAMD Radeon R9 M375X\n6821,\t00,\tAMD Radeon R9 M200X Series\n6821,\t83,\tAMD Radeon R9 M370X\n6821,\t87,\tAMD Radeon R7 M380\n6822,\t00,\tAMD Radeon E8860\n6823,\t00,\tAMD Radeon R9 M200X Series\n6825,\t00,\tAMD Radeon HD 7800M Series\n6826,\t00,\tAMD Radeon HD 7700M Series\n6827,\t00,\tAMD Radeon HD 7800M Series\n6828,\t00,\tAMD FirePro W600\n682B,\t00,\tAMD Radeon HD 8800M Series\n682B,\t87,\tAMD Radeon R9 M360\n682C,\t00,\tAMD FirePro W4100\n682D,\t00,\tAMD Radeon HD 7700M Series\n682F,\t00,\tAMD Radeon HD 7700M Series\n6830,\t00,\tAMD Radeon 7800M Series\n6831,\t00,\tAMD Radeon 7700M Series\n6835,\t00,\tAMD Radeon R7 Series / HD 9000 Series\n6837,\t00,\tAMD Radeon HD 7700 Series\n683D,\t00,\tAMD Radeon HD 7700 Series\n683F,\t00,\tAMD Radeon HD 7700 Series\n684C,\t00,\tATI FirePro V (FireGL V) Graphics Adapter\n6860,\t00,\tAMD Radeon Instinct MI25\n6860,\t01,\tAMD Radeon Instinct MI25\n6860,\t02,\tAMD Radeon Instinct MI25\n6860,\t03,\tAMD Radeon Pro V340\n6860,\t04,\tAMD Radeon Instinct MI25x2\n6860,\t07,\tAMD Radeon Pro V320\n6861,\t00,\tAMD Radeon Pro WX 9100\n6862,\t00,\tAMD Radeon Pro SSG\n6863,\t00,\tAMD Radeon Vega Frontier Edition\n6864,\t03,\tAMD Radeon Pro V340\n6864,\t04,\tAMD Radeon Instinct MI25x2\n6864,\t05,\tAMD Radeon Pro V340\n6868,\t00,\tAMD Radeon Pro WX 8200\n686C,\t00,\tAMD Radeon Instinct MI25 MxGPU\n686C,\t01,\tAMD Radeon Instinct MI25 MxGPU\n686C,\t02,\tAMD Radeon Instinct MI25 MxGPU\n686C,\t03,\tAMD Radeon Pro V340 MxGPU\n686C,\t04,\tAMD Radeon Instinct MI25x2 MxGPU\n686C,\t05,\tAMD Radeon Pro V340L MxGPU\n686C,\t06,\tAMD Radeon Instinct MI25 MxGPU\n687F,\t01,\tAMD Radeon RX Vega\n687F,\tC0,\tAMD Radeon RX Vega\n687F,\tC1,\tAMD Radeon RX Vega\n687F,\tC3,\tAMD Radeon RX Vega\n687F,\tC7,\tAMD Radeon RX Vega\n6900,\t00,\tAMD Radeon R7 M260\n6900,\t81,\tAMD Radeon R7 M360\n6900,\t83,\tAMD Radeon R7 M340\n6900,\tC1,\tAMD Radeon R5 M465 Series\n6900,\tC3,\tAMD Radeon R5 M445 Series\n6900,\tD1,\tAMD Radeon 530 Series\n6900,\tD3,\tAMD Radeon 530 Series\n6901,\t00,\tAMD Radeon R5 M255\n6902,\t00,\tAMD Radeon Series\n6907,\t00,\tAMD Radeon R5 M255\n6907,\t87,\tAMD Radeon R5 M315\n6920,\t00,\tAMD Radeon R9 M395X\n6920,\t01,\tAMD Radeon R9 M390X\n6921,\t00,\tAMD Radeon R9 M390X\n6929,\t00,\tAMD FirePro S7150\n6929,\t01,\tAMD FirePro S7100X\n692B,\t00,\tAMD FirePro W7100\n6938,\t00,\tAMD Radeon R9 200 Series\n6938,\tF0,\tAMD Radeon R9 200 Series\n6938,\tF1,\tAMD Radeon R9 380 Series\n6939,\t00,\tAMD Radeon R9 200 Series\n6939,\tF0,\tAMD Radeon R9 200 Series\n6939,\tF1,\tAMD Radeon R9 380 Series\n694C,\tC0,\tAMD Radeon RX Vega M GH Graphics\n694E,\tC0,\tAMD Radeon RX Vega M GL Graphics\n6980,\t00,\tAMD Radeon Pro WX 3100\n6981,\t00,\tAMD Radeon Pro WX 3200 Series\n6981,\t01,\tAMD Radeon Pro WX 3200 Series\n6981,\t10,\tAMD Radeon Pro WX 3200 Series\n6985,\t00,\tAMD Radeon Pro WX 3100\n6986,\t00,\tAMD Radeon Pro WX 2100\n6987,\t80,\tAMD Embedded Radeon E9171\n6987,\tC0,\tAMD Radeon 550X Series\n6987,\tC1,\tAMD Radeon RX 640\n6987,\tC3,\tAMD Radeon 540X Series\n6987,\tC7,\tAMD Radeon 540\n6995,\t00,\tAMD Radeon Pro WX 2100\n6997,\t00,\tAMD Radeon Pro WX 2100\n699F,\t81,\tAMD Embedded Radeon E9170 Series\n699F,\tC0,\tAMD Radeon 500 Series\n699F,\tC1,\tAMD Radeon 540 Series\n699F,\tC3,\tAMD Radeon 500 Series\n699F,\tC7,\tAMD Radeon RX 550 / 550 Series\n699F,\tC9,\tAMD Radeon 540\n6FDF,\tE7,\tAMD Radeon RX 590 GME\n6FDF,\tEF,\tAMD Radeon RX 580 2048SP\n7300,\tC1,\tAMD FirePro S9300 x2\n7300,\tC8,\tAMD Radeon R9 Fury Series\n7300,\tC9,\tAMD Radeon Pro Duo\n7300,\tCA,\tAMD Radeon R9 Fury Series\n7300,\tCB,\tAMD Radeon R9 Fury Series\n7312,\t00,\tAMD Radeon Pro W5700\n731E,\tC6,\tAMD Radeon RX 5700XTB\n731E,\tC7,\tAMD Radeon RX 5700B\n731F,\tC0,\tAMD Radeon RX 5700 XT 50th Anniversary\n731F,\tC1,\tAMD Radeon RX 5700 XT\n731F,\tC2,\tAMD Radeon RX 5600M\n731F,\tC3,\tAMD Radeon RX 5700M\n731F,\tC4,\tAMD Radeon RX 5700\n731F,\tC5,\tAMD Radeon RX 5700 XT\n731F,\tCA,\tAMD Radeon RX 5600 XT\n731F,\tCB,\tAMD Radeon RX 5600 OEM\n7340,\tC1,\tAMD Radeon RX 5500M\n7340,\tC3,\tAMD Radeon RX 5300M\n7340,\tC5,\tAMD Radeon RX 5500 XT\n7340,\tC7,\tAMD Radeon RX 5500\n7340,\tC9,\tAMD Radeon RX 5500XTB\n7340,\tCF,\tAMD Radeon RX 5300\n7341,\t00,\tAMD Radeon Pro W5500\n7347,\t00,\tAMD Radeon Pro W5500M\n7360,\t41,\tAMD Radeon Pro 5600M\n7360,\tC3,\tAMD Radeon Pro V520\n7362,\tC1,\tAMD Radeon Pro V540\n7362,\tC3,\tAMD Radeon Pro V520\n738C,\t01,\tAMD Instinct MI100\n73A1,\t00,\tAMD Radeon Pro V620\n73A3,\t00,\tAMD Radeon Pro W6800\n73A5,\tC0,\tAMD Radeon RX 6950 XT\n73AE,\t00,\tAMD Radeon Pro V620 MxGPU\n73AF,\tC0,\tAMD Radeon RX 6900 XT\n73BF,\tC0,\tAMD Radeon RX 6900 XT\n73BF,\tC1,\tAMD Radeon RX 6800 XT\n73BF,\tC3,\tAMD Radeon RX 6800\n73DF,\tC0,\tAMD Radeon RX 6750 XT\n73DF,\tC1,\tAMD Radeon RX 6700 XT\n73DF,\tC2,\tAMD Radeon RX 6800M\n73DF,\tC3,\tAMD Radeon RX 6800M\n73DF,\tC5,\tAMD Radeon RX 6700 XT\n73DF,\tCF,\tAMD Radeon RX 6700M\n73DF,\tD5,\tAMD Radeon RX 6750 GRE 12GB\n73DF,\tD7,\tAMD TDC-235\n73DF,\tDF,\tAMD Radeon RX 6700\n73DF,\tE5,\tAMD Radeon RX 6750 GRE 12GB\n73DF,\tFF,\tAMD Radeon RX 6700\n73E0,\t00,\tAMD Radeon RX 6600M\n73E1,\t00,\tAMD Radeon Pro W6600M\n73E3,\t00,\tAMD Radeon Pro W6600\n73EF,\tC0,\tAMD Radeon RX 6800S\n73EF,\tC1,\tAMD Radeon RX 6650 XT\n73EF,\tC2,\tAMD Radeon RX 6700S\n73EF,\tC3,\tAMD Radeon RX 6650M\n73EF,\tC4,\tAMD Radeon RX 6650M XT\n73FF,\tC1,\tAMD Radeon RX 6600 XT\n73FF,\tC3,\tAMD Radeon RX 6600M\n73FF,\tC7,\tAMD Radeon RX 6600\n73FF,\tCB,\tAMD Radeon RX 6600S\n73FF,\tCF,\tAMD Radeon RX 6600 LE\n73FF,\tDF,\tAMD Radeon RX 6750 GRE 10GB\n7408,\t00,\tAMD Instinct MI250X\n740C,\t01,\tAMD Instinct MI250X / MI250\n740F,\t02,\tAMD Instinct MI210\n7421,\t00,\tAMD Radeon Pro W6500M\n7422,\t00,\tAMD Radeon Pro W6400\n7423,\t00,\tAMD Radeon Pro W6300M\n7423,\t01,\tAMD Radeon Pro W6300\n7424,\t00,\tAMD Radeon RX 6300\n743F,\tC1,\tAMD Radeon RX 6500 XT\n743F,\tC3,\tAMD Radeon RX 6500\n743F,\tC3,\tAMD Radeon RX 6500M\n743F,\tC7,\tAMD Radeon RX 6400\n743F,\tC8,\tAMD Radeon RX 6500M\n743F,\tCC,\tAMD Radeon 6550S\n743F,\tCE,\tAMD Radeon RX 6450M\n743F,\tCF,\tAMD Radeon RX 6300M\n743F,\tD3,\tAMD Radeon RX 6550M\n743F,\tD7,\tAMD Radeon RX 6400\n7448,\t00,\tAMD Radeon Pro W7900\n7449,\t00,\tAMD Radeon Pro W7800 48GB\n744A,\t00,\tAMD Radeon Pro W7900 Dual Slot\n744B,\t00,\tAMD Radeon Pro W7900D\n744C,\tC8,\tAMD Radeon RX 7900 XTX\n744C,\tCC,\tAMD Radeon RX 7900 XT\n744C,\tCE,\tAMD Radeon RX 7900 GRE\n744C,\tCF,\tAMD Radeon RX 7900M\n745E,\tCC,\tAMD Radeon Pro W7800\n7460,\t00,\tAMD Radeon Pro V710\n7461,\t00,\tAMD Radeon Pro V710 MxGPU\n7470,\t00,\tAMD Radeon Pro W7700\n747E,\tC8,\tAMD Radeon RX 7800 XT\n747E,\tD8,\tAMD Radeon RX 7800M\n747E,\tDB,\tAMD Radeon RX 7700\n747E,\tFF,\tAMD Radeon RX 7700 XT\n7480,\t00,\tAMD Radeon Pro W7600\n7480,\tC0,\tAMD Radeon RX 7600 XT\n7480,\tC1,\tAMD Radeon RX 7700S\n7480,\tC2,\tAMD Radeon RX 7650 GRE\n7480,\tC3,\tAMD Radeon RX 7600S\n7480,\tC7,\tAMD Radeon RX 7600M XT\n7480,\tCF,\tAMD Radeon RX 7600\n7481,   C7,     AMD Steam Machine\n7483,\tCF,\tAMD Radeon RX 7600M\n7489,\t00,\tAMD Radeon Pro W7500\n7499,\t00,\tAMD Radeon Pro W7400\n7499,\tC0,\tAMD Radeon RX 7400\n7499,\tC1,\tAMD Radeon RX 7300\n74A0,\t00,\tAMD Instinct MI300A\n74A1,\t00,\tAMD Instinct MI300X\n74A2,\t00,\tAMD Instinct MI308X\n74A5,\t00,\tAMD Instinct MI325X\n74A8,\t00,\tAMD Instinct MI308X HF\n74A9,\t00,\tAMD Instinct MI300X HF\n74B5,\t00,\tAMD Instinct MI300X VF\n74B6,\t00,\tAMD Instinct MI308X\n74BD,\t00,\tAMD Instinct MI300X HF\n7550,\tC0,\tAMD Radeon RX 9070 XT\n7550,\tC2,\tAMD Radeon RX 9070 GRE\n7550,\tC3,\tAMD Radeon RX 9070\n7551,\tC0,\tAMD Radeon AI PRO R9700\n7590,\tC0,\tAMD Radeon RX 9060 XT\n7590,\tC7,\tAMD Radeon RX 9060\n75A0,\tC0,\tAMD Instinct MI350X\n75A3,\tC0,\tAMD Instinct MI355X\n75B0,\tC0,\tAMD Instinct MI350X VF\n75B3,\tC0,\tAMD Instinct MI355X VF\n9830,\t00,\tAMD Radeon HD 8400 / R3 Series\n9831,\t00,\tAMD Radeon HD 8400E\n9832,\t00,\tAMD Radeon HD 8330\n9833,\t00,\tAMD Radeon HD 8330E\n9834,\t00,\tAMD Radeon HD 8210\n9835,\t00,\tAMD Radeon HD 8210E\n9836,\t00,\tAMD Radeon HD 8200 / R3 Series\n9837,\t00,\tAMD Radeon HD 8280E\n9838,\t00,\tAMD Radeon HD 8200 / R3 series\n9839,\t00,\tAMD Radeon HD 8180\n983D,\t00,\tAMD Radeon HD 8250\n9850,\t00,\tAMD Radeon R3 Graphics\n9850,\t03,\tAMD Radeon R3 Graphics\n9850,\t40,\tAMD Radeon R2 Graphics\n9850,\t45,\tAMD Radeon R3 Graphics\n9851,\t00,\tAMD Radeon R4 Graphics\n9851,\t01,\tAMD Radeon R5E Graphics\n9851,\t05,\tAMD Radeon R5 Graphics\n9851,\t06,\tAMD Radeon R5E Graphics\n9851,\t40,\tAMD Radeon R4 Graphics\n9851,\t45,\tAMD Radeon R5 Graphics\n9852,\t00,\tAMD Radeon R2 Graphics\n9852,\t40,\tAMD Radeon E1 Graphics\n9853,\t00,\tAMD Radeon R2 Graphics\n9853,\t01,\tAMD Radeon R4E Graphics\n9853,\t03,\tAMD Radeon R2 Graphics\n9853,\t05,\tAMD Radeon R1E Graphics\n9853,\t06,\tAMD Radeon R1E Graphics\n9853,\t07,\tAMD Radeon R1E Graphics\n9853,\t08,\tAMD Radeon R1E Graphics\n9853,\t40,\tAMD Radeon R2 Graphics\n9854,\t00,\tAMD Radeon R3 Graphics\n9854,\t01,\tAMD Radeon R3E Graphics\n9854,\t02,\tAMD Radeon R3 Graphics\n9854,\t05,\tAMD Radeon R2 Graphics\n9854,\t06,\tAMD Radeon R4 Graphics\n9854,\t07,\tAMD Radeon R3 Graphics\n9855,\t02,\tAMD Radeon R6 Graphics\n9855,\t05,\tAMD Radeon R4 Graphics\n9856,\t00,\tAMD Radeon R2 Graphics\n9856,\t01,\tAMD Radeon R2E Graphics\n9856,\t02,\tAMD Radeon R2 Graphics\n9856,\t05,\tAMD Radeon R1E Graphics\n9856,\t06,\tAMD Radeon R2 Graphics\n9856,\t07,\tAMD Radeon R1E Graphics\n9856,\t08,\tAMD Radeon R1E Graphics\n9856,\t13,\tAMD Radeon R1E Graphics\n9874,\t81,\tAMD Radeon R6 Graphics\n9874,\t84,\tAMD Radeon R7 Graphics\n9874,\t85,\tAMD Radeon R6 Graphics\n9874,\t87,\tAMD Radeon R5 Graphics\n9874,\t88,\tAMD Radeon R7E Graphics\n9874,\t89,\tAMD Radeon R6E Graphics\n9874,\tC4,\tAMD Radeon R7 Graphics\n9874,\tC5,\tAMD Radeon R6 Graphics\n9874,\tC6,\tAMD Radeon R6 Graphics\n9874,\tC7,\tAMD Radeon R5 Graphics\n9874,\tC8,\tAMD Radeon R7 Graphics\n9874,\tC9,\tAMD Radeon R7 Graphics\n9874,\tCA,\tAMD Radeon R5 Graphics\n9874,\tCB,\tAMD Radeon R5 Graphics\n9874,\tCC,\tAMD Radeon R7 Graphics\n9874,\tCD,\tAMD Radeon R7 Graphics\n9874,\tCE,\tAMD Radeon R5 Graphics\n9874,\tE1,\tAMD Radeon R7 Graphics\n9874,\tE2,\tAMD Radeon R7 Graphics\n9874,\tE3,\tAMD Radeon R7 Graphics\n9874,\tE4,\tAMD Radeon R7 Graphics\n9874,\tE5,\tAMD Radeon R5 Graphics\n9874,\tE6,\tAMD Radeon R5 Graphics\n98E4,\t80,\tAMD Radeon R5E Graphics\n98E4,\t81,\tAMD Radeon R4E Graphics\n98E4,\t83,\tAMD Radeon R2E Graphics\n98E4,\t84,\tAMD Radeon R2E Graphics\n98E4,\t86,\tAMD Radeon R1E Graphics\n98E4,\tC0,\tAMD Radeon R4 Graphics\n98E4,\tC1,\tAMD Radeon R5 Graphics\n98E4,\tC2,\tAMD Radeon R4 Graphics\n98E4,\tC4,\tAMD Radeon R5 Graphics\n98E4,\tC6,\tAMD Radeon R5 Graphics\n98E4,\tC8,\tAMD Radeon R4 Graphics\n98E4,\tC9,\tAMD Radeon R4 Graphics\n98E4,\tCA,\tAMD Radeon R5 Graphics\n98E4,\tD0,\tAMD Radeon R2 Graphics\n98E4,\tD1,\tAMD Radeon R2 Graphics\n98E4,\tD2,\tAMD Radeon R2 Graphics\n98E4,\tD4,\tAMD Radeon R2 Graphics\n98E4,\tD9,\tAMD Radeon R5 Graphics\n98E4,\tDA,\tAMD Radeon R5 Graphics\n98E4,\tDB,\tAMD Radeon R3 Graphics\n98E4,\tE1,\tAMD Radeon R3 Graphics\n98E4,\tE2,\tAMD Radeon R3 Graphics\n98E4,\tE9,\tAMD Radeon R4 Graphics\n98E4,\tEA,\tAMD Radeon R4 Graphics\n98E4,\tEB,\tAMD Radeon R3 Graphics\n98E4,\tEB,\tAMD Radeon R4 Graphics\n"
  },
  {
    "path": "agent/test-data/container.json",
    "content": "{\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\"memory_stats\": {\n\t\t\"usage\": 507400192,\n\t\t\"stats\": {\n\t\t\t\"inactive_file\": 165130240\n\t\t}\n\t},\n\t\"networks\": {\n\t\t\"eth0\": {\n\t\t\t\"tx_bytes\": 20376558,\n\t\t\t\"rx_bytes\": 537029455\n\t\t},\n\t\t\"eth1\": {\n\t\t\t\"tx_bytes\": 2003766,\n\t\t\t\"rx_bytes\": 6241\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agent/test-data/container2.json",
    "content": "{\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\"memory_stats\": {\n\t\t\"usage\": 507400192,\n\t\t\"stats\": {\n\t\t\t\"inactive_file\": 165130240\n\t\t}\n\t},\n\t\"networks\": {\n\t\t\"eth0\": {\n\t\t\t\"tx_bytes\": 20376558,\n\t\t\t\"rx_bytes\": 537029455\n\t\t},\n\t\t\"eth1\": {\n\t\t\t\"tx_bytes\": 2003766,\n\t\t\t\"rx_bytes\": 6241\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "agent/test-data/nvtop.json",
    "content": "[\n  {\n   \"device_name\": \"NVIDIA GeForce RTX 3050 Ti Laptop GPU\",\n   \"gpu_clock\": \"1485MHz\",\n   \"mem_clock\": \"6001MHz\",\n   \"temp\": \"48C\",\n   \"fan_speed\": null,\n   \"power_draw\": \"13W\",\n   \"gpu_util\": \"5%\",\n   \"encode\": \"0%\",\n   \"decode\": \"0%\",\n   \"mem_util\": \"8%\",\n   \"mem_total\": \"4294967296\",\n   \"mem_used\": \"349372416\",\n   \"mem_free\": \"3945594880\",\n   \"processes\" : []\n  },\n  {\n   \"device_name\": \"AMD Radeon 680M\",\n   \"gpu_clock\": \"2200MHz\",\n   \"mem_clock\": \"2400MHz\",\n   \"temp\": \"48C\",\n   \"fan_speed\": \"CPU Fan\",\n   \"power_draw\": \"9W\",\n   \"gpu_util\": \"12%\",\n   \"encode\": null,\n   \"decode\": \"0%\",\n   \"mem_util\": \"7%\",\n   \"mem_total\": \"16929173504\",\n   \"mem_used\": \"1213784064\",\n   \"mem_free\": \"15715389440\",\n   \"processes\" : []\n  }\n]\n"
  },
  {
    "path": "agent/test-data/smart/nvme0.json",
    "content": "{\n  \"json_format_version\": [\n    1,\n    0\n  ],\n  \"smartctl\": {\n    \"version\": [\n      7,\n      5\n    ],\n    \"pre_release\": false,\n    \"svn_revision\": \"5714\",\n    \"platform_info\": \"x86_64-linux-6.17.1-2-cachyos\",\n    \"build_info\": \"(local build)\",\n    \"argv\": [\n      \"smartctl\",\n      \"-aj\",\n      \"/dev/nvme0\"\n    ],\n    \"exit_status\": 0\n  },\n  \"local_time\": {\n    \"time_t\": 1761507494,\n    \"asctime\": \"Sun Oct 26 15:38:14 2025 EDT\"\n  },\n  \"device\": {\n    \"name\": \"/dev/nvme0\",\n    \"info_name\": \"/dev/nvme0\",\n    \"type\": \"nvme\",\n    \"protocol\": \"NVMe\"\n  },\n  \"model_name\": \"PELADN 512GB\",\n  \"serial_number\": \"2024031600129\",\n  \"firmware_version\": \"VC2S038E\",\n  \"nvme_pci_vendor\": {\n    \"id\": 4332,\n    \"subsystem_id\": 4332\n  },\n  \"nvme_ieee_oui_identifier\": 57420,\n  \"nvme_controller_id\": 1,\n  \"nvme_version\": {\n    \"string\": \"1.4\",\n    \"value\": 66560\n  },\n  \"nvme_number_of_namespaces\": 1,\n  \"nvme_namespaces\": [\n    {\n      \"id\": 1,\n      \"size\": {\n        \"blocks\": 1000215216,\n        \"bytes\": 512110190592\n      },\n      \"capacity\": {\n        \"blocks\": 1000215216,\n        \"bytes\": 512110190592\n      },\n      \"utilization\": {\n        \"blocks\": 1000215216,\n        \"bytes\": 512110190592\n      },\n      \"formatted_lba_size\": 512,\n      \"eui64\": {\n        \"oui\": 57420,\n        \"ext_id\": 112094110470\n      },\n      \"features\": {\n        \"value\": 0,\n        \"thin_provisioning\": false,\n        \"na_fields\": false,\n        \"dealloc_or_unwritten_block_error\": false,\n        \"uid_reuse\": false,\n        \"np_fields\": false,\n        \"other\": 0\n      },\n      \"lba_formats\": [\n        {\n          \"formatted\": true,\n          \"data_bytes\": 512,\n          \"metadata_bytes\": 0,\n          \"relative_performance\": 0\n        }\n      ]\n    }\n  ],\n  \"user_capacity\": {\n    \"blocks\": 1000215216,\n    \"bytes\": 512110190592\n  },\n  \"logical_block_size\": 512,\n  \"smart_support\": {\n    \"available\": true,\n    \"enabled\": true\n  },\n  \"nvme_firmware_update_capabilities\": {\n    \"value\": 2,\n    \"slots\": 1,\n    \"first_slot_is_read_only\": false,\n    \"activiation_without_reset\": false,\n    \"multiple_update_detection\": false,\n    \"other\": 0\n  },\n  \"nvme_optional_admin_commands\": {\n    \"value\": 23,\n    \"security_send_receive\": true,\n    \"format_nvm\": true,\n    \"firmware_download\": true,\n    \"namespace_management\": false,\n    \"self_test\": true,\n    \"directives\": false,\n    \"mi_send_receive\": false,\n    \"virtualization_management\": false,\n    \"doorbell_buffer_config\": false,\n    \"get_lba_status\": false,\n    \"command_and_feature_lockdown\": false,\n    \"other\": 0\n  },\n  \"nvme_optional_nvm_commands\": {\n    \"value\": 94,\n    \"compare\": false,\n    \"write_uncorrectable\": true,\n    \"dataset_management\": true,\n    \"write_zeroes\": true,\n    \"save_select_feature_nonzero\": true,\n    \"reservations\": false,\n    \"timestamp\": true,\n    \"verify\": false,\n    \"copy\": false,\n    \"other\": 0\n  },\n  \"nvme_log_page_attributes\": {\n    \"value\": 2,\n    \"smart_health_per_namespace\": false,\n    \"commands_effects_log\": true,\n    \"extended_get_log_page_cmd\": false,\n    \"telemetry_log\": false,\n    \"persistent_event_log\": false,\n    \"supported_log_pages_log\": false,\n    \"telemetry_data_area_4\": false,\n    \"other\": 0\n  },\n  \"nvme_maximum_data_transfer_pages\": 32,\n  \"nvme_composite_temperature_threshold\": {\n    \"warning\": 100,\n    \"critical\": 110\n  },\n  \"temperature\": {\n    \"op_limit_max\": 100,\n    \"critical_limit_max\": 110,\n    \"current\": 61\n  },\n  \"nvme_power_states\": [\n    {\n      \"non_operational_state\": false,\n      \"relative_read_latency\": 0,\n      \"relative_read_throughput\": 0,\n      \"relative_write_latency\": 0,\n      \"relative_write_throughput\": 0,\n      \"entry_latency_us\": 230000,\n      \"exit_latency_us\": 50000,\n      \"max_power\": {\n        \"value\": 800,\n        \"scale\": 2,\n        \"units_per_watt\": 100\n      }\n    },\n    {\n      \"non_operational_state\": false,\n      \"relative_read_latency\": 1,\n      \"relative_read_throughput\": 1,\n      \"relative_write_latency\": 1,\n      \"relative_write_throughput\": 1,\n      \"entry_latency_us\": 4000,\n      \"exit_latency_us\": 50000,\n      \"max_power\": {\n        \"value\": 400,\n        \"scale\": 2,\n        \"units_per_watt\": 100\n      }\n    },\n    {\n      \"non_operational_state\": false,\n      \"relative_read_latency\": 2,\n      \"relative_read_throughput\": 2,\n      \"relative_write_latency\": 2,\n      \"relative_write_throughput\": 2,\n      \"entry_latency_us\": 4000,\n      \"exit_latency_us\": 250000,\n      \"max_power\": {\n        \"value\": 300,\n        \"scale\": 2,\n        \"units_per_watt\": 100\n      }\n    },\n    {\n      \"non_operational_state\": true,\n      \"relative_read_latency\": 3,\n      \"relative_read_throughput\": 3,\n      \"relative_write_latency\": 3,\n      \"relative_write_throughput\": 3,\n      \"entry_latency_us\": 5000,\n      \"exit_latency_us\": 10000,\n      \"max_power\": {\n        \"value\": 300,\n        \"scale\": 1,\n        \"units_per_watt\": 10000\n      }\n    },\n    {\n      \"non_operational_state\": true,\n      \"relative_read_latency\": 4,\n      \"relative_read_throughput\": 4,\n      \"relative_write_latency\": 4,\n      \"relative_write_throughput\": 4,\n      \"entry_latency_us\": 54000,\n      \"exit_latency_us\": 45000,\n      \"max_power\": {\n        \"value\": 50,\n        \"scale\": 1,\n        \"units_per_watt\": 10000\n      }\n    }\n  ],\n  \"smart_status\": {\n    \"passed\": true,\n    \"nvme\": {\n      \"value\": 0\n    }\n  },\n  \"nvme_smart_health_information_log\": {\n    \"nsid\": -1,\n    \"critical_warning\": 0,\n    \"temperature\": 61,\n    \"available_spare\": 100,\n    \"available_spare_threshold\": 32,\n    \"percentage_used\": 0,\n    \"data_units_read\": 6573104,\n    \"data_units_written\": 16040567,\n    \"host_reads\": 63241130,\n    \"host_writes\": 253050006,\n    \"controller_busy_time\": 0,\n    \"power_cycles\": 430,\n    \"power_on_hours\": 4399,\n    \"unsafe_shutdowns\": 44,\n    \"media_errors\": 0,\n    \"num_err_log_entries\": 0,\n    \"warning_temp_time\": 0,\n    \"critical_comp_time\": 0\n  },\n  \"spare_available\": {\n    \"current_percent\": 100,\n    \"threshold_percent\": 32\n  },\n  \"endurance_used\": {\n    \"current_percent\": 0\n  },\n  \"power_cycle_count\": 430,\n  \"power_on_time\": {\n    \"hours\": 4399\n  },\n  \"nvme_error_information_log\": {\n    \"size\": 8,\n    \"read\": 8,\n    \"unread\": 0\n  },\n  \"nvme_self_test_log\": {\n    \"nsid\": -1,\n    \"current_self_test_operation\": {\n      \"value\": 0,\n      \"string\": \"No self-test in progress\"\n    }\n  }\n}\n"
  },
  {
    "path": "agent/test-data/smart/scan.json",
    "content": "{\n  \"json_format_version\": [\n    1,\n    0\n  ],\n  \"smartctl\": {\n    \"version\": [\n      7,\n      5\n    ],\n    \"pre_release\": false,\n    \"svn_revision\": \"5714\",\n    \"platform_info\": \"x86_64-linux-6.17.1-2-cachyos\",\n    \"build_info\": \"(local build)\",\n    \"argv\": [\n      \"smartctl\",\n      \"--scan\",\n      \"-j\"\n    ],\n    \"exit_status\": 0\n  },\n  \"devices\": [\n    {\n      \"name\": \"/dev/sda\",\n      \"info_name\": \"/dev/sda [SAT]\",\n      \"type\": \"sat\",\n      \"protocol\": \"ATA\"\n    },\n    {\n      \"name\": \"/dev/nvme0\",\n      \"info_name\": \"/dev/nvme0\",\n      \"type\": \"nvme\",\n      \"protocol\": \"NVMe\"\n    }\n  ]\n}\n"
  },
  {
    "path": "agent/test-data/smart/scsi.json",
    "content": "{\n   \"json_format_version\": [\n      1,\n      0\n   ],\n   \"smartctl\": {\n      \"version\": [\n         7,\n         3\n      ],\n      \"svn_revision\": \"5338\",\n      \"platform_info\": \"x86_64-linux-6.12.43+deb12-amd64\",\n      \"build_info\": \"(local build)\",\n      \"argv\": [\n         \"smartctl\",\n         \"-aj\",\n         \"/dev/sde\"\n      ],\n      \"exit_status\": 0\n   },\n   \"local_time\": {\n      \"time_t\": 1761502142,\n      \"asctime\": \"Sun Oct 21 21:09:02 2025 MSK\"\n   },\n   \"device\": {\n      \"name\": \"/dev/sde\",\n      \"info_name\": \"/dev/sde\",\n      \"type\": \"scsi\",\n      \"protocol\": \"SCSI\"\n   },\n   \"scsi_vendor\": \"YADRO\",\n   \"scsi_product\": \"WUH721414AL4204\",\n   \"scsi_model_name\": \"YADRO WUH721414AL4204\",\n   \"scsi_revision\": \"C240\",\n   \"scsi_version\": \"SPC-4\",\n   \"user_capacity\": {\n      \"blocks\": 3418095616,\n      \"bytes\": 14000519643136\n   },\n   \"logical_block_size\": 4096,\n   \"scsi_lb_provisioning\": {\n      \"name\": \"fully provisioned\",\n      \"value\": 0,\n      \"management_enabled\": {\n         \"name\": \"LBPME\",\n         \"value\": 0\n      },\n      \"read_zeros\": {\n         \"name\": \"LBPRZ\",\n         \"value\": 0\n      }\n   },\n   \"rotation_rate\": 7200,\n   \"form_factor\": {\n      \"scsi_value\": 2,\n      \"name\": \"3.5 inches\"\n   },\n   \"logical_unit_id\": \"0x5000cca29063dc00\",\n   \"serial_number\": \"9YHSDH9B\",\n   \"device_type\": {\n      \"scsi_terminology\": \"Peripheral Device Type [PDT]\",\n      \"scsi_value\": 0,\n      \"name\": \"disk\"\n   },\n   \"scsi_transport_protocol\": {\n      \"name\": \"SAS (SPL-4)\",\n      \"value\": 6\n   },\n   \"smart_support\": {\n      \"available\": true,\n      \"enabled\": true\n   },\n   \"temperature_warning\": {\n      \"enabled\": true\n   },\n   \"smart_status\": {\n      \"passed\": true\n   },\n   \"temperature\": {\n      \"current\": 34,\n      \"drive_trip\": 85\n   },\n   \"power_on_time\": {\n      \"hours\": 458,\n      \"minutes\": 25\n   },\n   \"scsi_start_stop_cycle_counter\": {\n      \"year_of_manufacture\": \"2022\",\n      \"week_of_manufacture\": \"41\",\n      \"specified_cycle_count_over_device_lifetime\": 50000,\n      \"accumulated_start_stop_cycles\": 2,\n      \"specified_load_unload_count_over_device_lifetime\": 600000,\n      \"accumulated_load_unload_cycles\": 418\n   },\n   \"scsi_grown_defect_list\": 0,\n   \"scsi_error_counter_log\": {\n      \"read\": {\n         \"errors_corrected_by_eccfast\": 0,\n         \"errors_corrected_by_eccdelayed\": 0,\n         \"errors_corrected_by_rereads_rewrites\": 0,\n         \"total_errors_corrected\": 0,\n         \"correction_algorithm_invocations\": 346,\n         \"gigabytes_processed\": \"3,641\",\n         \"total_uncorrected_errors\": 0\n      },\n      \"write\": {\n         \"errors_corrected_by_eccfast\": 0,\n         \"errors_corrected_by_eccdelayed\": 0,\n         \"errors_corrected_by_rereads_rewrites\": 0,\n         \"total_errors_corrected\": 0,\n         \"correction_algorithm_invocations\": 4052,\n         \"gigabytes_processed\": \"2124,590\",\n         \"total_uncorrected_errors\": 0\n      },\n      \"verify\": {\n         \"errors_corrected_by_eccfast\": 0,\n         \"errors_corrected_by_eccdelayed\": 0,\n         \"errors_corrected_by_rereads_rewrites\": 0,\n         \"total_errors_corrected\": 0,\n         \"correction_algorithm_invocations\": 223,\n         \"gigabytes_processed\": \"0,000\",\n         \"total_uncorrected_errors\": 0\n      }\n   }\n}"
  },
  {
    "path": "agent/test-data/smart/sda.json",
    "content": "{\n  \"json_format_version\": [\n    1,\n    0\n  ],\n  \"smartctl\": {\n    \"version\": [\n      7,\n      5\n    ],\n    \"pre_release\": false,\n    \"svn_revision\": \"5714\",\n    \"platform_info\": \"x86_64-linux-6.17.1-2-cachyos\",\n    \"build_info\": \"(local build)\",\n    \"argv\": [\n      \"smartctl\",\n      \"-aj\",\n      \"/dev/sda\"\n    ],\n    \"drive_database_version\": {\n      \"string\": \"7.5/5706\"\n    },\n    \"messages\": [\n      {\n        \"string\": \"Warning: This result is based on an Attribute check.\",\n        \"severity\": \"warning\"\n      }\n    ],\n    \"exit_status\": 64\n  },\n  \"local_time\": {\n    \"time_t\": 1761507466,\n    \"asctime\": \"Sun Oct 26 15:37:46 2025 EDT\"\n  },\n  \"device\": {\n    \"name\": \"/dev/sda\",\n    \"info_name\": \"/dev/sda [SAT]\",\n    \"type\": \"sat\",\n    \"protocol\": \"ATA\"\n  },\n  \"model_name\": \"P3-2TB\",\n  \"serial_number\": \"9C40918040082\",\n  \"firmware_version\": \"X0104A0\",\n  \"user_capacity\": {\n    \"blocks\": 4000797360,\n    \"bytes\": 2048408248320\n  },\n  \"logical_block_size\": 512,\n  \"physical_block_size\": 512,\n  \"rotation_rate\": 0,\n  \"form_factor\": {\n    \"ata_value\": 3,\n    \"name\": \"2.5 inches\"\n  },\n  \"trim\": {\n    \"supported\": true,\n    \"deterministic\": false,\n    \"zeroed\": false\n  },\n  \"in_smartctl_database\": false,\n  \"ata_version\": {\n    \"string\": \"ACS-2 T13/2015-D revision 3\",\n    \"major_value\": 1008,\n    \"minor_value\": 272\n  },\n  \"sata_version\": {\n    \"string\": \"SATA 3.2\",\n    \"value\": 255\n  },\n  \"interface_speed\": {\n    \"max\": {\n      \"sata_value\": 14,\n      \"string\": \"6.0 Gb/s\",\n      \"units_per_second\": 60,\n      \"bits_per_unit\": 100000000\n    },\n    \"current\": {\n      \"sata_value\": 3,\n      \"string\": \"6.0 Gb/s\",\n      \"units_per_second\": 60,\n      \"bits_per_unit\": 100000000\n    }\n  },\n  \"smart_support\": {\n    \"available\": true,\n    \"enabled\": true\n  },\n  \"smart_status\": {\n    \"passed\": true\n  },\n  \"ata_smart_data\": {\n    \"offline_data_collection\": {\n      \"status\": {\n        \"value\": 0,\n        \"string\": \"was never started\"\n      },\n      \"completion_seconds\": 120\n    },\n    \"self_test\": {\n      \"status\": {\n        \"value\": 0,\n        \"string\": \"completed without error\",\n        \"passed\": true\n      },\n      \"polling_minutes\": {\n        \"short\": 2,\n        \"extended\": 10\n      }\n    },\n    \"capabilities\": {\n      \"values\": [\n        17,\n        2\n      ],\n      \"exec_offline_immediate_supported\": true,\n      \"offline_is_aborted_upon_new_cmd\": false,\n      \"offline_surface_scan_supported\": false,\n      \"self_tests_supported\": true,\n      \"conveyance_self_test_supported\": false,\n      \"selective_self_test_supported\": false,\n      \"attribute_autosave_enabled\": false,\n      \"error_logging_supported\": true,\n      \"gp_logging_supported\": true\n    }\n  },\n  \"ata_sct_capabilities\": {\n    \"value\": 1,\n    \"error_recovery_control_supported\": false,\n    \"feature_control_supported\": false,\n    \"data_table_supported\": false\n  },\n  \"ata_smart_attributes\": {\n    \"revision\": 1,\n    \"table\": [\n      {\n        \"id\": 1,\n        \"name\": \"Raw_Read_Error_Rate\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 5,\n        \"name\": \"Reallocated_Sector_Ct\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 9,\n        \"name\": \"Power_On_Hours\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 7344,\n          \"string\": \"7344\"\n        }\n      },\n      {\n        \"id\": 12,\n        \"name\": \"Power_Cycle_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 104,\n          \"string\": \"104\"\n        }\n      },\n      {\n        \"id\": 160,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 161,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 51,\n          \"string\": \"PO--CK \",\n          \"prefailure\": true,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 100,\n          \"string\": \"100\"\n        }\n      },\n      {\n        \"id\": 163,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 12,\n          \"string\": \"12\"\n        }\n      },\n      {\n        \"id\": 164,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 4140,\n          \"string\": \"4140\"\n        }\n      },\n      {\n        \"id\": 165,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 3,\n          \"string\": \"3\"\n        }\n      },\n      {\n        \"id\": 166,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 2,\n          \"string\": \"2\"\n        }\n      },\n      {\n        \"id\": 167,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 2,\n          \"string\": \"2\"\n        }\n      },\n      {\n        \"id\": 168,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 5050,\n          \"string\": \"5050\"\n        }\n      },\n      {\n        \"id\": 169,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 100,\n          \"string\": \"100\"\n        }\n      },\n      {\n        \"id\": 175,\n        \"name\": \"Program_Fail_Count_Chip\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 176,\n        \"name\": \"Erase_Fail_Count_Chip\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 177,\n        \"name\": \"Wear_Leveling_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 178,\n        \"name\": \"Used_Rsvd_Blk_Cnt_Chip\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 181,\n        \"name\": \"Program_Fail_Cnt_Total\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 182,\n        \"name\": \"Erase_Fail_Count_Total\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 192,\n        \"name\": \"Power-Off_Retract_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 98,\n          \"string\": \"98\"\n        }\n      },\n      {\n        \"id\": 194,\n        \"name\": \"Temperature_Celsius\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 34,\n          \"string\": \"-O---K \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": false,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 31,\n          \"string\": \"31\"\n        }\n      },\n      {\n        \"id\": 195,\n        \"name\": \"Hardware_ECC_Recovered\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 295843,\n          \"string\": \"295843\"\n        }\n      },\n      {\n        \"id\": 196,\n        \"name\": \"Reallocated_Event_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 197,\n        \"name\": \"Current_Pending_Sector\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 198,\n        \"name\": \"Offline_Uncorrectable\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 0,\n          \"string\": \"0\"\n        }\n      },\n      {\n        \"id\": 199,\n        \"name\": \"UDMA_CRC_Error_Count\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 131,\n          \"string\": \"131\"\n        }\n      },\n      {\n        \"id\": 232,\n        \"name\": \"Available_Reservd_Space\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 100,\n          \"string\": \"100\"\n        }\n      },\n      {\n        \"id\": 241,\n        \"name\": \"Total_LBAs_Written\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 48,\n          \"string\": \"----CK \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 37763,\n          \"string\": \"37763\"\n        }\n      },\n      {\n        \"id\": 242,\n        \"name\": \"Total_LBAs_Read\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 48,\n          \"string\": \"----CK \",\n          \"prefailure\": false,\n          \"updated_online\": false,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 3928,\n          \"string\": \"3928\"\n        }\n      },\n      {\n        \"id\": 245,\n        \"name\": \"Unknown_Attribute\",\n        \"value\": 100,\n        \"worst\": 100,\n        \"thresh\": 50,\n        \"when_failed\": \"\",\n        \"flags\": {\n          \"value\": 50,\n          \"string\": \"-O--CK \",\n          \"prefailure\": false,\n          \"updated_online\": true,\n          \"performance\": false,\n          \"error_rate\": false,\n          \"event_count\": true,\n          \"auto_keep\": true\n        },\n        \"raw\": {\n          \"value\": 25604,\n          \"string\": \"25604\"\n        }\n      }\n    ]\n  },\n  \"spare_available\": {\n    \"current_percent\": 100\n  },\n  \"power_on_time\": {\n    \"hours\": 7344\n  },\n  \"power_cycle_count\": 104,\n  \"endurance_used\": {\n    \"current_percent\": 0\n  },\n  \"temperature\": {\n    \"current\": 31\n  },\n  \"ata_smart_error_log\": {\n    \"summary\": {\n      \"revision\": 1,\n      \"count\": 131,\n      \"logged_count\": 5,\n      \"table\": [\n        {\n          \"error_number\": 129,\n          \"lifetime_hours\": 0,\n          \"completion_registers\": {\n            \"error\": 4,\n            \"status\": 81,\n            \"count\": 0,\n            \"lba\": 0,\n            \"device\": 64\n          },\n          \"error_description\": \"Error: ABRT\",\n          \"previous_commands\": [\n            {\n              \"registers\": {\n                \"command\": 176,\n                \"features\": 208,\n                \"count\": 1,\n                \"lba\": 12734208,\n                \"device\": 0,\n                \"device_control\": 8\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"SMART READ DATA\"\n            },\n            {\n              \"registers\": {\n                \"command\": 176,\n                \"features\": 209,\n                \"count\": 1,\n                \"lba\": 12734209,\n                \"device\": 0,\n                \"device_control\": 8\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"SMART READ ATTRIBUTE THRESHOLDS [OBS-4]\"\n            },\n            {\n              \"registers\": {\n                \"command\": 176,\n                \"features\": 218,\n                \"count\": 0,\n                \"lba\": 12734208,\n                \"device\": 0,\n                \"device_control\": 8\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"SMART RETURN STATUS\"\n            },\n            {\n              \"registers\": {\n                \"command\": 176,\n                \"features\": 213,\n                \"count\": 1,\n                \"lba\": 12734208,\n                \"device\": 0,\n                \"device_control\": 8\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"SMART READ LOG\"\n            },\n            {\n              \"registers\": {\n                \"command\": 236,\n                \"features\": 0,\n                \"count\": 1,\n                \"lba\": 0,\n                \"device\": 0,\n                \"device_control\": 8\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"IDENTIFY DEVICE\"\n            }\n          ]\n        },\n        {\n          \"error_number\": 127,\n          \"lifetime_hours\": 0,\n          \"completion_registers\": {\n            \"error\": 0,\n            \"status\": 0,\n            \"count\": 0,\n            \"lba\": 0,\n            \"device\": 0\n          },\n          \"error_description\": \" at LBA = 0x00000000 = 0\",\n          \"previous_commands\": [\n            {\n              \"registers\": {\n                \"command\": 97,\n                \"features\": 8,\n                \"count\": 0,\n                \"lba\": 919080,\n                \"device\": 0,\n                \"device_control\": 0\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"WRITE FPDMA QUEUED\"\n            },\n            {\n              \"registers\": {\n                \"command\": 97,\n                \"features\": 8,\n                \"count\": 0,\n                \"lba\": 919080,\n                \"device\": 0,\n                \"device_control\": 0\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"WRITE FPDMA QUEUED\"\n            },\n            {\n              \"registers\": {\n                \"command\": 97,\n                \"features\": 8,\n                \"count\": 0,\n                \"lba\": 919080,\n                \"device\": 0,\n                \"device_control\": 0\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"WRITE FPDMA QUEUED\"\n            },\n            {\n              \"registers\": {\n                \"command\": 97,\n                \"features\": 8,\n                \"count\": 0,\n                \"lba\": 919080,\n                \"device\": 0,\n                \"device_control\": 0\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"WRITE FPDMA QUEUED\"\n            },\n            {\n              \"registers\": {\n                \"command\": 97,\n                \"features\": 8,\n                \"count\": 0,\n                \"lba\": 919080,\n                \"device\": 0,\n                \"device_control\": 0\n              },\n              \"powerup_milliseconds\": 0,\n              \"command_name\": \"WRITE FPDMA QUEUED\"\n            }\n          ]\n        }\n      ]\n    }\n  },\n  \"ata_smart_self_test_log\": {\n    \"standard\": {\n      \"revision\": 1,\n      \"table\": [\n        {\n          \"type\": {\n            \"value\": 1,\n            \"string\": \"Short offline\"\n          },\n          \"status\": {\n            \"value\": 23,\n            \"string\": \"Aborted by host\",\n            \"remaining_percent\": 70\n          },\n          \"lifetime_hours\": 0\n        },\n        {\n          \"type\": {\n            \"value\": 1,\n            \"string\": \"Short offline\"\n          },\n          \"status\": {\n            \"value\": 23,\n            \"string\": \"Aborted by host\",\n            \"remaining_percent\": 70\n          },\n          \"lifetime_hours\": 0\n        },\n        {\n          \"type\": {\n            \"value\": 1,\n            \"string\": \"Short offline\"\n          },\n          \"status\": {\n            \"value\": 23,\n            \"string\": \"Aborted by host\",\n            \"remaining_percent\": 70\n          },\n          \"lifetime_hours\": 0\n        }\n      ],\n      \"count\": 3,\n      \"error_count_total\": 0,\n      \"error_count_outdated\": 0\n    }\n  }\n}\n"
  },
  {
    "path": "agent/test-data/system_info.json",
    "content": "{\n  \"ID\": \"7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS\",\n  \"Containers\": 14,\n  \"ContainersRunning\": 3,\n  \"ContainersPaused\": 1,\n  \"ContainersStopped\": 10,\n  \"Images\": 508,\n  \"Driver\": \"overlay2\",\n  \"KernelVersion\": \"6.8.0-31-generic\",\n  \"OperatingSystem\": \"Ubuntu 24.04 LTS\",\n  \"OSVersion\": \"24.04\",\n  \"OSType\": \"linux\",\n  \"Architecture\": \"x86_64\",\n  \"NCPU\": 4,\n  \"MemTotal\": 2095882240,\n  \"ServerVersion\": \"27.0.1\"\n}\n"
  },
  {
    "path": "agent/tools/fetchsmartctl/main.go",
    "content": "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/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Download smartctl.exe from the given URL and save it to the given destination.\n// This is used to embed smartctl.exe in the Windows build.\n\nfunc main() {\n\turl := flag.String(\"url\", \"\", \"URL to download smartctl.exe from (required)\")\n\tout := flag.String(\"out\", \"\", \"Destination path for smartctl.exe (required)\")\n\tsha := flag.String(\"sha\", \"\", \"Optional SHA1/SHA256 checksum for integrity validation\")\n\tforce := flag.Bool(\"force\", false, \"Force re-download even if destination exists\")\n\tflag.Parse()\n\n\tif *url == \"\" || *out == \"\" {\n\t\tfatalf(\"-url and -out are required\")\n\t}\n\n\tif !*force {\n\t\tif info, err := os.Stat(*out); err == nil && info.Size() > 0 {\n\t\t\tfmt.Println(\"smartctl.exe already present, skipping download\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := downloadFile(*url, *out, *sha); err != nil {\n\t\tfatalf(\"download failed: %v\", err)\n\t}\n}\n\nfunc downloadFile(url, dest, shaHex string) error {\n\t// Prepare destination\n\tif err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"create dir: %w\", err)\n\t}\n\n\t// HTTP client\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"new request: %w\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", \"beszel-fetchsmartctl/1.0\")\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"http get: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn fmt.Errorf(\"unexpected HTTP status: %s\", resp.Status)\n\t}\n\n\ttmp := dest + \".tmp\"\n\tf, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open tmp: %w\", err)\n\t}\n\n\t// Determine hash algorithm based on length (SHA1=40, SHA256=64)\n\tvar hasher hash.Hash\n\tif shaHex := strings.TrimSpace(shaHex); shaHex != \"\" {\n\t\tcleanSha := strings.ToLower(strings.ReplaceAll(shaHex, \" \", \"\"))\n\t\tswitch len(cleanSha) {\n\t\tcase 40:\n\t\t\thasher = sha1.New()\n\t\tcase 64:\n\t\t\thasher = sha256.New()\n\t\tdefault:\n\t\t\tf.Close()\n\t\t\tos.Remove(tmp)\n\t\t\treturn fmt.Errorf(\"unsupported hash length: %d (expected 40 for SHA1 or 64 for SHA256)\", len(cleanSha))\n\t\t}\n\t}\n\n\tvar mw io.Writer = f\n\tif hasher != nil {\n\t\tmw = io.MultiWriter(f, hasher)\n\t}\n\tif _, err := io.Copy(mw, resp.Body); err != nil {\n\t\tf.Close()\n\t\tos.Remove(tmp)\n\t\treturn fmt.Errorf(\"write tmp: %w\", err)\n\t}\n\tif err := f.Close(); err != nil {\n\t\tos.Remove(tmp)\n\t\treturn fmt.Errorf(\"close tmp: %w\", err)\n\t}\n\n\tif hasher != nil && shaHex != \"\" {\n\t\tcleanSha := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(shaHex), \" \", \"\"))\n\t\tgot := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))\n\t\tif got != cleanSha {\n\t\t\tos.Remove(tmp)\n\t\t\treturn fmt.Errorf(\"hash mismatch: got %s want %s\", got, cleanSha)\n\t\t}\n\t}\n\n\t// Make executable and move into place\n\tif err := os.Chmod(tmp, 0o755); err != nil {\n\t\tos.Remove(tmp)\n\t\treturn fmt.Errorf(\"chmod: %w\", err)\n\t}\n\tif err := os.Rename(tmp, dest); err != nil {\n\t\tos.Remove(tmp)\n\t\treturn fmt.Errorf(\"rename: %w\", err)\n\t}\n\n\tfmt.Println(\"smartctl.exe downloaded to\", dest)\n\treturn nil\n}\n\nfunc fatalf(format string, a ...any) {\n\tfmt.Fprintf(os.Stderr, format+\"\\n\", a...)\n\tos.Exit(1)\n}\n"
  },
  {
    "path": "agent/update.go",
    "content": "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// restarter knows how to restart the beszel-agent service.\ntype restarter interface {\n\tRestart() error\n}\n\ntype systemdRestarter struct{ cmd string }\n\nfunc (s *systemdRestarter) Restart() error {\n\t// Only restart if the service is active\n\tif err := exec.Command(s.cmd, \"is-active\", \"beszel-agent.service\").Run(); err != nil {\n\t\treturn nil\n\t}\n\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel-agent.service via systemd…\")\n\treturn exec.Command(s.cmd, \"restart\", \"beszel-agent.service\").Run()\n}\n\ntype openRCRestarter struct{ cmd string }\n\nfunc (o *openRCRestarter) Restart() error {\n\tif err := exec.Command(o.cmd, \"beszel-agent\", \"status\").Run(); err != nil {\n\t\treturn nil\n\t}\n\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel-agent via OpenRC…\")\n\treturn exec.Command(o.cmd, \"beszel-agent\", \"restart\").Run()\n}\n\ntype openWRTRestarter struct{ cmd string }\n\nfunc (w *openWRTRestarter) Restart() error {\n\t// https://openwrt.org/docs/guide-user/base-system/managing_services?s[]=service\n\tif err := exec.Command(\"/etc/init.d/beszel-agent\", \"running\").Run(); err != nil {\n\t\treturn nil\n\t}\n\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel-agent via procd…\")\n\treturn exec.Command(\"/etc/init.d/beszel-agent\", \"restart\").Run()\n}\n\ntype freeBSDRestarter struct{ cmd string }\n\nfunc (f *freeBSDRestarter) Restart() error {\n\tif err := exec.Command(f.cmd, \"beszel-agent\", \"status\").Run(); err != nil {\n\t\treturn nil\n\t}\n\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel-agent via FreeBSD rc…\")\n\treturn exec.Command(f.cmd, \"beszel-agent\", \"restart\").Run()\n}\n\nfunc detectRestarter() restarter {\n\tif path, err := exec.LookPath(\"systemctl\"); err == nil {\n\t\treturn &systemdRestarter{cmd: path}\n\t}\n\tif path, err := exec.LookPath(\"rc-service\"); err == nil {\n\t\treturn &openRCRestarter{cmd: path}\n\t}\n\tif path, err := exec.LookPath(\"procd\"); err == nil {\n\t\treturn &openWRTRestarter{cmd: path}\n\t}\n\tif path, err := exec.LookPath(\"service\"); err == nil {\n\t\tif runtime.GOOS == \"freebsd\" {\n\t\t\treturn &freeBSDRestarter{cmd: path}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Update checks GitHub for a newer release of beszel-agent, applies it,\n// fixes SELinux context if needed, and restarts the service.\nfunc Update(useMirror bool) error {\n\texePath, _ := os.Executable()\n\n\tdataDir, err := GetDataDir()\n\tif err != nil {\n\t\tdataDir = os.TempDir()\n\t}\n\tupdated, err := ghupdate.Update(ghupdate.Config{\n\t\tArchiveExecutable: \"beszel-agent\",\n\t\tDataDir:           dataDir,\n\t\tUseMirror:         useMirror,\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif !updated {\n\t\treturn nil\n\t}\n\n\t// make sure the file is executable\n\tif err := os.Chmod(exePath, 0755); err != nil {\n\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: failed to set executable permissions: %v\", err)\n\t}\n\t// set ownership to beszel:beszel if possible\n\tif chownPath, err := exec.LookPath(\"chown\"); err == nil {\n\t\tif err := exec.Command(chownPath, \"beszel:beszel\", exePath).Run(); err != nil {\n\t\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: failed to set file ownership: %v\", err)\n\t\t}\n\t}\n\n\t// Fix SELinux context if necessary\n\tif err := ghupdate.HandleSELinuxContext(exePath); err != nil {\n\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: SELinux context handling: %v\", err)\n\t}\n\n\t// Restart service if running under a recognised init system\n\tif r := detectRestarter(); r != nil {\n\t\tif err := r.Restart(); err != nil {\n\t\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: failed to restart service: %v\", err)\n\t\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Please restart the service manually.\")\n\t\t} else {\n\t\t\tghupdate.ColorPrint(ghupdate.ColorGreen, \"Service restarted successfully\")\n\t\t}\n\t} else {\n\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"No supported init system detected; please restart manually if needed.\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "agent/utils/utils.go",
    "content": "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 \"BESZEL_AGENT_\" prefix, or falls back to the unprefixed key.\nfunc GetEnv(key string) (value string, exists bool) {\n\tif value, exists = os.LookupEnv(\"BESZEL_AGENT_\" + key); exists {\n\t\treturn value, exists\n\t}\n\treturn os.LookupEnv(key)\n}\n\n// BytesToMegabytes converts bytes to megabytes and rounds to two decimal places.\nfunc BytesToMegabytes(b float64) float64 {\n\treturn TwoDecimals(b / 1048576)\n}\n\n// BytesToGigabytes converts bytes to gigabytes and rounds to two decimal places.\nfunc BytesToGigabytes(b uint64) float64 {\n\treturn TwoDecimals(float64(b) / 1073741824)\n}\n\n// TwoDecimals rounds a float64 value to two decimal places.\nfunc TwoDecimals(value float64) float64 {\n\treturn math.Round(value*100) / 100\n}\n\n// func RoundFloat(val float64, precision uint) float64 {\n//     ratio := math.Pow(10, float64(precision))\n//     return math.Round(val*ratio) / ratio\n// }\n\n// ReadStringFile returns trimmed file contents or empty string on error.\nfunc ReadStringFile(path string) string {\n\tcontent, _ := ReadStringFileOK(path)\n\treturn content\n}\n\n// ReadStringFileOK returns trimmed file contents and read success.\nfunc ReadStringFileOK(path string) (string, bool) {\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\treturn strings.TrimSpace(string(b)), true\n}\n\n// ReadStringFileLimited reads a file into a string with a maximum size (in bytes) to avoid\n// allocating large buffers and potential panics with pseudo-files when the size is misreported.\nfunc ReadStringFileLimited(path string, maxSize int) (string, error) {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\n\tbuf := make([]byte, maxSize)\n\tn, err := f.Read(buf)\n\tif err != nil && err != io.EOF {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(buf[:n])), nil\n}\n\n// FileExists reports whether the given path exists.\nfunc FileExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn err == nil\n}\n\n// ReadUintFile parses a decimal uint64 value from a file.\nfunc ReadUintFile(path string) (uint64, bool) {\n\traw, ok := ReadStringFileOK(path)\n\tif !ok {\n\t\treturn 0, false\n\t}\n\tparsed, err := strconv.ParseUint(raw, 10, 64)\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\treturn parsed, true\n}\n"
  },
  {
    "path": "agent/utils/utils_test.go",
    "content": "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(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    float64\n\t\texpected float64\n\t}{\n\t\t{\"round down\", 1.234, 1.23},\n\t\t{\"round half up\", 1.235, 1.24}, // math.Round rounds half up\n\t\t{\"no rounding needed\", 1.23, 1.23},\n\t\t{\"negative number\", -1.235, -1.24}, // math.Round rounds half up (more negative)\n\t\t{\"zero\", 0.0, 0.0},\n\t\t{\"large number\", 123.456, 123.46}, // rounds 5 up\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := TwoDecimals(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestBytesToMegabytes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    float64\n\t\texpected float64\n\t}{\n\t\t{\"1 MB\", 1048576, 1.0},\n\t\t{\"512 KB\", 524288, 0.5},\n\t\t{\"zero\", 0, 0},\n\t\t{\"large value\", 1073741824, 1024}, // 1 GB = 1024 MB\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := BytesToMegabytes(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestBytesToGigabytes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    uint64\n\t\texpected float64\n\t}{\n\t\t{\"1 GB\", 1073741824, 1.0},\n\t\t{\"512 MB\", 536870912, 0.5},\n\t\t{\"0 GB\", 0, 0},\n\t\t{\"2 GB\", 2147483648, 2.0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := BytesToGigabytes(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestFileFunctions(t *testing.T) {\n\ttmpDir := t.TempDir()\n\ttestFilePath := filepath.Join(tmpDir, \"test.txt\")\n\ttestContent := \"hello world\"\n\n\t// Test FileExists (false)\n\tassert.False(t, FileExists(testFilePath))\n\n\t// Test ReadStringFileOK (false)\n\tcontent, ok := ReadStringFileOK(testFilePath)\n\tassert.False(t, ok)\n\tassert.Empty(t, content)\n\n\t// Test ReadStringFile (empty)\n\tassert.Empty(t, ReadStringFile(testFilePath))\n\n\t// Write file\n\terr := os.WriteFile(testFilePath, []byte(testContent+\"\\n \"), 0644)\n\tassert.NoError(t, err)\n\n\t// Test FileExists (true)\n\tassert.True(t, FileExists(testFilePath))\n\n\t// Test ReadStringFileOK (true)\n\tcontent, ok = ReadStringFileOK(testFilePath)\n\tassert.True(t, ok)\n\tassert.Equal(t, testContent, content)\n\n\t// Test ReadStringFile (content)\n\tassert.Equal(t, testContent, ReadStringFile(testFilePath))\n}\n\nfunc TestReadUintFile(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\tt.Run(\"valid uint\", func(t *testing.T) {\n\t\tpath := filepath.Join(tmpDir, \"uint.txt\")\n\t\tos.WriteFile(path, []byte(\" 12345\\n\"), 0644)\n\t\tval, ok := ReadUintFile(path)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, uint64(12345), val)\n\t})\n\n\tt.Run(\"invalid uint\", func(t *testing.T) {\n\t\tpath := filepath.Join(tmpDir, \"invalid.txt\")\n\t\tos.WriteFile(path, []byte(\"abc\"), 0644)\n\t\tval, ok := ReadUintFile(path)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, uint64(0), val)\n\t})\n\n\tt.Run(\"missing file\", func(t *testing.T) {\n\t\tpath := filepath.Join(tmpDir, \"missing.txt\")\n\t\tval, ok := ReadUintFile(path)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, uint64(0), val)\n\t})\n}\n\nfunc TestGetEnv(t *testing.T) {\n\tkey := \"TEST_VAR\"\n\tprefixedKey := \"BESZEL_AGENT_\" + key\n\n\tt.Run(\"prefixed variable exists\", func(t *testing.T) {\n\t\tos.Setenv(prefixedKey, \"prefixed_val\")\n\t\tos.Setenv(key, \"unprefixed_val\")\n\t\tdefer os.Unsetenv(prefixedKey)\n\t\tdefer os.Unsetenv(key)\n\n\t\tval, exists := GetEnv(key)\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, \"prefixed_val\", val)\n\t})\n\n\tt.Run(\"only unprefixed variable exists\", func(t *testing.T) {\n\t\tos.Unsetenv(prefixedKey)\n\t\tos.Setenv(key, \"unprefixed_val\")\n\t\tdefer os.Unsetenv(key)\n\n\t\tval, exists := GetEnv(key)\n\t\tassert.True(t, exists)\n\t\tassert.Equal(t, \"unprefixed_val\", val)\n\t})\n\n\tt.Run(\"neither variable exists\", func(t *testing.T) {\n\t\tos.Unsetenv(prefixedKey)\n\t\tos.Unsetenv(key)\n\n\t\tval, exists := GetEnv(key)\n\t\tassert.False(t, exists)\n\t\tassert.Empty(t, val)\n\t})\n}\n"
  },
  {
    "path": "agent/zfs/zfs_freebsd.go",
    "content": "//go:build freebsd\n\npackage zfs\n\nimport (\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc ARCSize() (uint64, error) {\n\treturn unix.SysctlUint64(\"kstat.zfs.misc.arcstats.size\")\n}\n"
  },
  {
    "path": "agent/zfs/zfs_linux.go",
    "content": "//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\t\"strconv\"\n\t\"strings\"\n)\n\nfunc ARCSize() (uint64, error) {\n\tfile, err := os.Open(\"/proc/spl/kstat/zfs/arcstats\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer file.Close()\n\n\tscanner := bufio.NewScanner(file)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"size\") {\n\t\t\tfields := strings.Fields(line)\n\t\t\tif len(fields) < 3 {\n\t\t\t\treturn 0, fmt.Errorf(\"unexpected arcstats size format: %s\", line)\n\t\t\t}\n\t\t\treturn strconv.ParseUint(fields[2], 10, 64)\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"size field not found in arcstats\")\n}\n"
  },
  {
    "path": "agent/zfs/zfs_unsupported.go",
    "content": "//go:build !linux && !freebsd\n\npackage zfs\n\nimport \"errors\"\n\nfunc ARCSize() (uint64, error) {\n\treturn 0, errors.ErrUnsupported\n}\n"
  },
  {
    "path": "beszel.go",
    "content": "// Package beszel provides core application constants and version information\n// which are used throughout the application.\npackage beszel\n\nimport \"github.com/blang/semver\"\n\nconst (\n\t// Version is the current version of the application.\n\tVersion = \"0.18.4\"\n\t// AppName is the name of the application.\n\tAppName = \"beszel\"\n)\n\n// MinVersionCbor is the minimum supported version for CBOR compatibility.\nvar MinVersionCbor = semver.MustParse(\"0.12.0\")\n\n// MinVersionAgentResponse is the minimum supported version for AgentResponse compatibility.\nvar MinVersionAgentResponse = semver.MustParse(\"0.13.0\")\n"
  },
  {
    "path": "go.mod",
    "content": "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/go-systemd/v22 v22.7.0\n\tgithub.com/distatus/battery v0.11.0\n\tgithub.com/ebitengine/purego v0.9.1\n\tgithub.com/fxamacker/cbor/v2 v2.9.0\n\tgithub.com/gliderlabs/ssh v0.3.8\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/lxzan/gws v1.8.9\n\tgithub.com/nicholas-fedor/shoutrrr v0.13.2\n\tgithub.com/pocketbase/dbx v1.12.0\n\tgithub.com/pocketbase/pocketbase v0.36.4\n\tgithub.com/shirou/gopsutil/v4 v4.26.1\n\tgithub.com/spf13/cast v1.10.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/crypto v0.48.0\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa\n\tgolang.org/x/sys v0.41.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/disintegration/imaging v1.6.2 // indirect\n\tgithub.com/dolthub/maphash v0.1.0 // indirect\n\tgithub.com/domodwyer/mailyak/v3 v3.6.2 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/ganigeorgiev/fexpr v0.5.0 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.9.1 // indirect\n\tgithub.com/godbus/dbus/v5 v5.2.2 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgolang.org/x/image v0.36.0 // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/oauth2 v0.35.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/term v0.40.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\thowett.net/plist v1.0.1 // indirect\n\tmodernc.org/libc v1.67.6 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n\tmodernc.org/sqlite v1.45.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=\ngithub.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/distatus/battery v0.11.0 h1:KJk89gz90Iq/wJtbjjM9yUzBXV+ASV/EG2WOOL7N8lc=\ngithub.com/distatus/battery v0.11.0/go.mod h1:KmVkE8A8hpIX4T78QRdMktYpEp35QfOL8A8dwZBxq2k=\ngithub.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=\ngithub.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=\ngithub.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=\ngithub.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=\ngithub.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=\ngithub.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=\ngithub.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=\ngithub.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=\ngithub.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=\ngithub.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=\ngithub.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/nicholas-fedor/shoutrrr v0.13.2 h1:hfsYBIqSFYGg92pZP5CXk/g7/OJIkLYmiUnRl+AD1IA=\ngithub.com/nicholas-fedor/shoutrrr v0.13.2/go.mod h1:ZqzV3gY/Wj6AvWs1etlO7+yKbh4iptSbeL8avBpMQbA=\ngithub.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=\ngithub.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=\ngithub.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=\ngithub.com/pocketbase/pocketbase v0.36.4 h1:zTjRZbp2WfTOJJfb+pFRWa200UaQwxZYt8RzkFMlAZ4=\ngithub.com/pocketbase/pocketbase v0.36.4/go.mod h1:9CiezhRudd9FZGa5xZa53QZBTNxc5vvw/FGG+diAECI=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=\ngithub.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=\ngolang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=\ngolang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhowett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=\nhowett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=\nmodernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=\nmodernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\n"
  },
  {
    "path": "i18n.yml",
    "content": "files:\n  - source: /internal/site/src/locales/en/\n    translation: /internal/site/src/locales/%two_letters_code%/%two_letters_code%.po\n"
  },
  {
    "path": "internal/alerts/alerts.go",
    "content": "// 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\t\"time\"\n\n\t\"github.com/nicholas-fedor/shoutrrr\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/mailer\"\n)\n\ntype hubLike interface {\n\tcore.App\n\tMakeLink(parts ...string) string\n}\n\ntype AlertManager struct {\n\thub           hubLike\n\tstopOnce      sync.Once\n\tpendingAlerts sync.Map\n\talertsCache   *AlertsCache\n}\n\ntype AlertMessageData struct {\n\tUserID   string\n\tSystemID string\n\tTitle    string\n\tMessage  string\n\tLink     string\n\tLinkText string\n}\n\ntype UserNotificationSettings struct {\n\tEmails   []string `json:\"emails\"`\n\tWebhooks []string `json:\"webhooks\"`\n}\n\ntype SystemAlertFsStats struct {\n\tDiskTotal float64 `json:\"d\"`\n\tDiskUsed  float64 `json:\"du\"`\n}\n\n// Values pulled from system_stats.stats that are relevant to alerts.\ntype SystemAlertStats struct {\n\tCpu          float64                       `json:\"cpu\"`\n\tMem          float64                       `json:\"mp\"`\n\tDisk         float64                       `json:\"dp\"`\n\tBandwidth    [2]uint64                     `json:\"b\"`\n\tGPU          map[string]SystemAlertGPUData `json:\"g\"`\n\tTemperatures map[string]float32            `json:\"t\"`\n\tLoadAvg      [3]float64                    `json:\"la\"`\n\tBattery      [2]uint8                      `json:\"bat\"`\n\tExtraFs      map[string]SystemAlertFsStats `json:\"efs\"`\n}\n\ntype SystemAlertGPUData struct {\n\tUsage float64 `json:\"u\"`\n}\n\ntype SystemAlertData struct {\n\tsystemRecord *core.Record\n\talertData    CachedAlertData\n\tname         string\n\tunit         string\n\tval          float64\n\tthreshold    float64\n\ttriggered    bool\n\ttime         time.Time\n\tcount        uint8\n\tmin          uint8\n\tmapSums      map[string]float32\n\tdescriptor   string // override descriptor in notification body (for temp sensor, disk partition, etc)\n}\n\n// notification services that support title param\nvar supportsTitle = map[string]struct{}{\n\t\"bark\":       {},\n\t\"discord\":    {},\n\t\"gotify\":     {},\n\t\"ifttt\":      {},\n\t\"join\":       {},\n\t\"lark\":       {},\n\t\"ntfy\":       {},\n\t\"opsgenie\":   {},\n\t\"pushbullet\": {},\n\t\"pushover\":   {},\n\t\"slack\":      {},\n\t\"teams\":      {},\n\t\"telegram\":   {},\n\t\"zulip\":      {},\n}\n\n// NewAlertManager creates a new AlertManager instance.\nfunc NewAlertManager(app hubLike) *AlertManager {\n\tam := &AlertManager{\n\t\thub:         app,\n\t\talertsCache: NewAlertsCache(app),\n\t}\n\tam.bindEvents()\n\treturn am\n}\n\n// Bind events to the alerts collection lifecycle\nfunc (am *AlertManager) bindEvents() {\n\tam.hub.OnRecordAfterUpdateSuccess(\"alerts\").BindFunc(updateHistoryOnAlertUpdate)\n\tam.hub.OnRecordAfterDeleteSuccess(\"alerts\").BindFunc(resolveHistoryOnAlertDelete)\n\tam.hub.OnRecordAfterUpdateSuccess(\"smart_devices\").BindFunc(am.handleSmartDeviceAlert)\n\n\tam.hub.OnServe().BindFunc(func(e *core.ServeEvent) error {\n\t\t// Populate all alerts into cache on startup\n\t\t_ = am.alertsCache.PopulateFromDB(true)\n\n\t\tif err := resolveStatusAlerts(e.App); err != nil {\n\t\t\te.App.Logger().Error(\"Failed to resolve stale status alerts\", \"err\", err)\n\t\t}\n\t\tif err := am.restorePendingStatusAlerts(); err != nil {\n\t\t\te.App.Logger().Error(\"Failed to restore pending status alerts\", \"err\", err)\n\t\t}\n\t\treturn e.Next()\n\t})\n}\n\n// IsNotificationSilenced checks if a notification should be silenced based on configured quiet hours\nfunc (am *AlertManager) IsNotificationSilenced(userID, systemID string) bool {\n\t// Query for quiet hours windows that match this user and system\n\t// Include both global windows (system is null/empty) and system-specific windows\n\tvar filter string\n\tvar params dbx.Params\n\n\tif systemID == \"\" {\n\t\t// If no systemID provided, only check global windows\n\t\tfilter = \"user={:user} AND system=''\"\n\t\tparams = dbx.Params{\"user\": userID}\n\t} else {\n\t\t// Check both global and system-specific windows\n\t\tfilter = \"user={:user} AND (system='' OR system={:system})\"\n\t\tparams = dbx.Params{\n\t\t\t\"user\":   userID,\n\t\t\t\"system\": systemID,\n\t\t}\n\t}\n\n\tquietHourWindows, err := am.hub.FindAllRecords(\"quiet_hours\", dbx.NewExp(filter, params))\n\tif err != nil || len(quietHourWindows) == 0 {\n\t\treturn false\n\t}\n\n\tnow := time.Now().UTC()\n\n\tfor _, window := range quietHourWindows {\n\t\twindowType := window.GetString(\"type\")\n\t\tstart := window.GetDateTime(\"start\").Time()\n\t\tend := window.GetDateTime(\"end\").Time()\n\n\t\tif windowType == \"daily\" {\n\t\t\t// For daily recurring windows, extract just the time portion and compare\n\t\t\t// The start/end are stored as full datetime but we only care about HH:MM\n\t\t\tstartHour, startMin, _ := start.Clock()\n\t\t\tendHour, endMin, _ := end.Clock()\n\t\t\tnowHour, nowMin, _ := now.Clock()\n\n\t\t\t// Convert to minutes since midnight for easier comparison\n\t\t\tstartMinutes := startHour*60 + startMin\n\t\t\tendMinutes := endHour*60 + endMin\n\t\t\tnowMinutes := nowHour*60 + nowMin\n\n\t\t\t// Handle case where window crosses midnight\n\t\t\tif endMinutes < startMinutes {\n\t\t\t\t// Window crosses midnight (e.g., 23:00 - 01:00)\n\t\t\t\tif nowMinutes >= startMinutes || nowMinutes < endMinutes {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Normal case (e.g., 09:00 - 17:00)\n\t\t\t\tif nowMinutes >= startMinutes && nowMinutes < endMinutes {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// One-time window: check if current time is within the date range\n\t\t\tif (now.After(start) || now.Equal(start)) && now.Before(end) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// SendAlert sends an alert to the user\nfunc (am *AlertManager) SendAlert(data AlertMessageData) error {\n\t// Check if alert is silenced\n\tif am.IsNotificationSilenced(data.UserID, data.SystemID) {\n\t\tam.hub.Logger().Info(\"Notification silenced\", \"user\", data.UserID, \"system\", data.SystemID, \"title\", data.Title)\n\t\treturn nil\n\t}\n\n\t// get user settings\n\trecord, err := am.hub.FindFirstRecordByFilter(\n\t\t\"user_settings\", \"user={:user}\",\n\t\tdbx.Params{\"user\": data.UserID},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// unmarshal user settings\n\tuserAlertSettings := UserNotificationSettings{\n\t\tEmails:   []string{},\n\t\tWebhooks: []string{},\n\t}\n\tif err := record.UnmarshalJSONField(\"settings\", &userAlertSettings); err != nil {\n\t\tam.hub.Logger().Error(\"Failed to unmarshal user settings\", \"err\", err)\n\t}\n\t// send alerts via webhooks\n\tfor _, webhook := range userAlertSettings.Webhooks {\n\t\tif err := am.SendShoutrrrAlert(webhook, data.Title, data.Message, data.Link, data.LinkText); err != nil {\n\t\t\tam.hub.Logger().Error(\"Failed to send shoutrrr alert\", \"err\", err)\n\t\t}\n\t}\n\t// send alerts via email\n\tif len(userAlertSettings.Emails) == 0 {\n\t\treturn nil\n\t}\n\taddresses := []mail.Address{}\n\tfor _, email := range userAlertSettings.Emails {\n\t\taddresses = append(addresses, mail.Address{Address: email})\n\t}\n\tmessage := mailer.Message{\n\t\tTo:      addresses,\n\t\tSubject: data.Title,\n\t\tText:    data.Message + fmt.Sprintf(\"\\n\\n%s\", data.Link),\n\t\tFrom: mail.Address{\n\t\t\tAddress: am.hub.Settings().Meta.SenderAddress,\n\t\t\tName:    am.hub.Settings().Meta.SenderName,\n\t\t},\n\t}\n\terr = am.hub.NewMailClient().Send(&message)\n\tif err != nil {\n\t\treturn err\n\t}\n\tam.hub.Logger().Info(\"Sent email alert\", \"to\", message.To, \"subj\", message.Subject)\n\treturn nil\n}\n\n// SendShoutrrrAlert sends an alert via a Shoutrrr URL\nfunc (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link, linkText string) error {\n\t// Parse the URL\n\tparsedURL, err := url.Parse(notificationUrl)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error parsing URL: %v\", err)\n\t}\n\tscheme := parsedURL.Scheme\n\tqueryParams := parsedURL.Query()\n\n\t// Add title\n\tif _, ok := supportsTitle[scheme]; ok {\n\t\tqueryParams.Add(\"title\", title)\n\t} else if scheme == \"mattermost\" {\n\t\t// use markdown title for mattermost\n\t\tmessage = \"##### \" + title + \"\\n\\n\" + message\n\t} else if scheme == \"generic\" && queryParams.Has(\"template\") {\n\t\t// add title as property if using generic with template json\n\t\ttitleKey := queryParams.Get(\"titlekey\")\n\t\tif titleKey == \"\" {\n\t\t\ttitleKey = \"title\"\n\t\t}\n\t\tqueryParams.Add(\"$\"+titleKey, title)\n\t} else {\n\t\t// otherwise just add title to message\n\t\tmessage = title + \"\\n\\n\" + message\n\t}\n\n\t// Add link\n\tswitch scheme {\n\tcase \"ntfy\":\n\t\tqueryParams.Add(\"Actions\", fmt.Sprintf(\"view, %s, %s\", linkText, link))\n\tcase \"lark\":\n\t\tqueryParams.Add(\"link\", link)\n\tcase \"bark\":\n\t\tqueryParams.Add(\"url\", link)\n\tdefault:\n\t\tmessage += \"\\n\\n\" + link\n\t}\n\n\t// Encode the modified query parameters back into the URL\n\tparsedURL.RawQuery = queryParams.Encode()\n\t// log.Println(\"URL after modification:\", parsedURL.String())\n\n\terr = shoutrrr.Send(parsedURL.String(), message)\n\n\tif err == nil {\n\t\tam.hub.Logger().Info(\"Sent shoutrrr alert\", \"title\", title)\n\t} else {\n\t\tam.hub.Logger().Error(\"Error sending shoutrrr alert\", \"err\", err)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (am *AlertManager) SendTestNotification(e *core.RequestEvent) error {\n\tvar data struct {\n\t\tURL string `json:\"url\"`\n\t}\n\terr := e.BindBody(&data)\n\tif err != nil || data.URL == \"\" {\n\t\treturn e.BadRequestError(\"URL is required\", err)\n\t}\n\terr = am.SendShoutrrrAlert(data.URL, \"Test Alert\", \"This is a notification from Beszel.\", am.hub.Settings().Meta.AppURL, \"View Beszel\")\n\tif err != nil {\n\t\treturn e.JSON(200, map[string]string{\"err\": err.Error()})\n\t}\n\treturn e.JSON(200, map[string]bool{\"err\": false})\n}\n\n// setAlertTriggered updates the \"triggered\" status of an alert record in the database\nfunc (am *AlertManager) setAlertTriggered(alert CachedAlertData, triggered bool) error {\n\talertRecord, err := am.hub.FindRecordById(\"alerts\", alert.Id)\n\tif err != nil {\n\t\treturn err\n\t}\n\talertRecord.Set(\"triggered\", triggered)\n\treturn am.hub.Save(alertRecord)\n}\n"
  },
  {
    "path": "internal/alerts/alerts_api.go",
    "content": "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/pocketbase/core\"\n)\n\n// UpsertUserAlerts handles API request to create or update alerts for a user\n// across multiple systems (POST /api/beszel/user-alerts)\nfunc UpsertUserAlerts(e *core.RequestEvent) error {\n\tuserID := e.Auth.Id\n\n\treqData := struct {\n\t\tMin       uint8    `json:\"min\"`\n\t\tValue     float64  `json:\"value\"`\n\t\tName      string   `json:\"name\"`\n\t\tSystems   []string `json:\"systems\"`\n\t\tOverwrite bool     `json:\"overwrite\"`\n\t}{}\n\terr := e.BindBody(&reqData)\n\tif err != nil || userID == \"\" || reqData.Name == \"\" || len(reqData.Systems) == 0 {\n\t\treturn e.BadRequestError(\"Bad data\", err)\n\t}\n\n\talertsCollection, err := e.App.FindCachedCollectionByNameOrId(\"alerts\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = e.App.RunInTransaction(func(txApp core.App) error {\n\t\tfor _, systemId := range reqData.Systems {\n\t\t\t// find existing matching alert\n\t\t\talertRecord, err := txApp.FindFirstRecordByFilter(alertsCollection,\n\t\t\t\t\"system={:system} && name={:name} && user={:user}\",\n\t\t\t\tdbx.Params{\"system\": systemId, \"name\": reqData.Name, \"user\": userID})\n\n\t\t\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// skip if alert already exists and overwrite is not set\n\t\t\tif !reqData.Overwrite && alertRecord != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// create new alert if it doesn't exist\n\t\t\tif alertRecord == nil {\n\t\t\t\talertRecord = core.NewRecord(alertsCollection)\n\t\t\t\talertRecord.Set(\"user\", userID)\n\t\t\t\talertRecord.Set(\"system\", systemId)\n\t\t\t\talertRecord.Set(\"name\", reqData.Name)\n\t\t\t}\n\n\t\t\talertRecord.Set(\"value\", reqData.Value)\n\t\t\talertRecord.Set(\"min\", reqData.Min)\n\n\t\t\tif err := txApp.SaveNoValidate(alertRecord); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn e.JSON(http.StatusOK, map[string]any{\"success\": true})\n}\n\n// DeleteUserAlerts handles API request to delete alerts for a user across multiple systems\n// (DELETE /api/beszel/user-alerts)\nfunc DeleteUserAlerts(e *core.RequestEvent) error {\n\tuserID := e.Auth.Id\n\n\treqData := struct {\n\t\tAlertName string   `json:\"name\"`\n\t\tSystems   []string `json:\"systems\"`\n\t}{}\n\terr := e.BindBody(&reqData)\n\tif err != nil || userID == \"\" || reqData.AlertName == \"\" || len(reqData.Systems) == 0 {\n\t\treturn e.BadRequestError(\"Bad data\", err)\n\t}\n\n\tvar numDeleted uint16\n\n\terr = e.App.RunInTransaction(func(txApp core.App) error {\n\t\tfor _, systemId := range reqData.Systems {\n\t\t\t// Find existing alert to delete\n\t\t\talertRecord, err := txApp.FindFirstRecordByFilter(\"alerts\",\n\t\t\t\t\"system={:system} && name={:name} && user={:user}\",\n\t\t\t\tdbx.Params{\"system\": systemId, \"name\": reqData.AlertName, \"user\": userID})\n\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t\t// alert doesn't exist, continue to next system\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := txApp.Delete(alertRecord); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnumDeleted++\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn e.JSON(http.StatusOK, map[string]any{\"success\": true, \"count\": numDeleted})\n}\n"
  },
  {
    "path": "internal/alerts/alerts_battery_test.go",
    "content": "//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/internal/entities/system\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/tools/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestBatteryAlertLogic tests that battery alerts trigger when value drops BELOW threshold\n// (opposite of other alerts like CPU, Memory, etc. which trigger when exceeding threshold)\nfunc TestBatteryAlertLogic(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\t// Create a battery alert with threshold of 20% and min of 1 minute (immediate trigger)\n\tbatteryAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Battery\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  20, // threshold: 20%\n\t\t\"min\":    1,  // 1 minute (immediate trigger for testing)\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify alert is not triggered initially\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\t// Create system stats with battery at 50% (above threshold - should NOT trigger)\n\tstatsHigh := system.Stats{\n\t\tCpu:     10,\n\t\tMemPct:  30,\n\t\tDiskPct: 40,\n\t\tBattery: [2]uint8{50, 1}, // 50% battery, discharging\n\t}\n\tstatsHighJSON, _ := json.Marshal(statsHigh)\n\t_, err = beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\"system\": systemRecord.Id,\n\t\t\"type\":   \"1m\",\n\t\t\"stats\":  string(statsHighJSON),\n\t})\n\trequire.NoError(t, err)\n\n\t// Create CombinedData for the alert handler\n\tcombinedDataHigh := &system.CombinedData{\n\t\tStats: statsHigh,\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Simulate system update time\n\tsystemRecord.Set(\"updated\", time.Now().UTC())\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts with high battery\n\tam := hub.GetAlertManager()\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataHigh)\n\trequire.NoError(t, err)\n\n\t// Verify alert is still NOT triggered (battery 50% is above threshold 20%)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"), \"Alert should NOT be triggered when battery (50%%) is above threshold (20%%)\")\n\n\t// Now create stats with battery at 15% (below threshold - should trigger)\n\tstatsLow := system.Stats{\n\t\tCpu:     10,\n\t\tMemPct:  30,\n\t\tDiskPct: 40,\n\t\tBattery: [2]uint8{15, 1}, // 15% battery, discharging\n\t}\n\tstatsLowJSON, _ := json.Marshal(statsLow)\n\t_, err = beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\"system\": systemRecord.Id,\n\t\t\"type\":   \"1m\",\n\t\t\"stats\":  string(statsLowJSON),\n\t})\n\trequire.NoError(t, err)\n\n\tcombinedDataLow := &system.CombinedData{\n\t\tStats: statsLow,\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Update system timestamp\n\tsystemRecord.Set(\"updated\", time.Now().UTC())\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts with low battery\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataLow)\n\trequire.NoError(t, err)\n\n\t// Wait for the alert to be processed\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Verify alert IS triggered (battery 15% is below threshold 20%)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.True(t, batteryAlert.GetBool(\"triggered\"), \"Alert SHOULD be triggered when battery (15%%) drops below threshold (20%%)\")\n\n\t// Now test resolution: battery goes back above threshold\n\tstatsRecovered := system.Stats{\n\t\tCpu:     10,\n\t\tMemPct:  30,\n\t\tDiskPct: 40,\n\t\tBattery: [2]uint8{25, 1}, // 25% battery, discharging\n\t}\n\tstatsRecoveredJSON, _ := json.Marshal(statsRecovered)\n\t_, err = beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\"system\": systemRecord.Id,\n\t\t\"type\":   \"1m\",\n\t\t\"stats\":  string(statsRecoveredJSON),\n\t})\n\trequire.NoError(t, err)\n\n\tcombinedDataRecovered := &system.CombinedData{\n\t\tStats: statsRecovered,\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Update system timestamp\n\tsystemRecord.Set(\"updated\", time.Now().UTC())\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts with recovered battery\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataRecovered)\n\trequire.NoError(t, err)\n\n\t// Wait for the alert to be processed\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Verify alert is now resolved (battery 25% is above threshold 20%)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"), \"Alert should be resolved when battery (25%%) goes above threshold (20%%)\")\n}\n\n// TestBatteryAlertNoBattery verifies that systems without battery data don't trigger alerts\nfunc TestBatteryAlertNoBattery(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\t// Create a battery alert\n\tbatteryAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Battery\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  20,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create stats with NO battery data (Battery[0] = 0)\n\tstatsNoBattery := system.Stats{\n\t\tCpu:     10,\n\t\tMemPct:  30,\n\t\tDiskPct: 40,\n\t\tBattery: [2]uint8{0, 0}, // No battery\n\t}\n\n\tcombinedData := &system.CombinedData{\n\t\tStats: statsNoBattery,\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Simulate system update time\n\tsystemRecord.Set(\"updated\", time.Now().UTC())\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts\n\tam := hub.GetAlertManager()\n\terr = am.HandleSystemAlerts(systemRecord, combinedData)\n\trequire.NoError(t, err)\n\n\t// Wait a moment for processing\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Verify alert is NOT triggered (no battery data should skip the alert)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"), \"Alert should NOT be triggered when system has no battery\")\n}\n\n// TestBatteryAlertAveragedSamples tests battery alerts with min > 1 (averaging multiple samples)\n// This ensures the inverted threshold logic works correctly across averaged time windows\nfunc TestBatteryAlertAveragedSamples(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\t// Create a battery alert with threshold of 25% and min of 2 minutes (requires averaging)\n\tbatteryAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Battery\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  25, // threshold: 25%\n\t\t\"min\":    2,  // 2 minutes - requires averaging\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify alert is not triggered initially\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\tam := hub.GetAlertManager()\n\tnow := time.Now().UTC()\n\n\t// Create system_stats records with low battery (below threshold)\n\t// The alert has min=2 minutes, so alert.time = now - 2 minutes\n\t// For the alert to be valid, alert.time must be AFTER the oldest record's created time\n\t// So we need records older than (now - 2 min), plus records within the window\n\t// Records at: now-3min (oldest, before window), now-90s, now-60s, now-30s\n\trecordTimes := []time.Duration{\n\t\t-180 * time.Second, // 3 min ago - this makes the oldest record before alert.time\n\t\t-90 * time.Second,\n\t\t-60 * time.Second,\n\t\t-30 * time.Second,\n\t}\n\n\tfor _, offset := range recordTimes {\n\t\tstatsLow := system.Stats{\n\t\t\tCpu:     10,\n\t\t\tMemPct:  30,\n\t\t\tDiskPct: 40,\n\t\t\tBattery: [2]uint8{15, 1}, // 15% battery (below 25% threshold)\n\t\t}\n\t\tstatsLowJSON, _ := json.Marshal(statsLow)\n\n\t\trecordTime := now.Add(offset)\n\t\trecord, err := beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\t\"system\": systemRecord.Id,\n\t\t\t\"type\":   \"1m\",\n\t\t\t\"stats\":  string(statsLowJSON),\n\t\t})\n\t\trequire.NoError(t, err)\n\t\t// Update created time to simulate historical records - use SetRaw with formatted string\n\t\trecord.SetRaw(\"created\", recordTime.Format(types.DefaultDateLayout))\n\t\terr = hub.SaveNoValidate(record)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create combined data with low battery\n\tcombinedDataLow := &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tCpu:     10,\n\t\t\tMemPct:  30,\n\t\t\tDiskPct: 40,\n\t\t\tBattery: [2]uint8{15, 1},\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Update system timestamp\n\tsystemRecord.Set(\"updated\", now)\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts - should trigger because average battery is below threshold\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataLow)\n\trequire.NoError(t, err)\n\n\t// Wait for alert processing\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Verify alert IS triggered (average battery 15% is below threshold 25%)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.True(t, batteryAlert.GetBool(\"triggered\"),\n\t\t\"Alert SHOULD be triggered when average battery (15%%) is below threshold (25%%) over min period\")\n\n\t// Now add records with high battery to test resolution\n\t// Use a new time window 2 minutes later\n\tnewNow := now.Add(2 * time.Minute)\n\t// Records need to span before the alert time window (newNow - 2 min)\n\trecordTimesHigh := []time.Duration{\n\t\t-180 * time.Second, // 3 min before newNow - makes oldest record before alert.time\n\t\t-90 * time.Second,\n\t\t-60 * time.Second,\n\t\t-30 * time.Second,\n\t}\n\n\tfor _, offset := range recordTimesHigh {\n\t\tstatsHigh := system.Stats{\n\t\t\tCpu:     10,\n\t\t\tMemPct:  30,\n\t\t\tDiskPct: 40,\n\t\t\tBattery: [2]uint8{50, 1}, // 50% battery (above 25% threshold)\n\t\t}\n\t\tstatsHighJSON, _ := json.Marshal(statsHigh)\n\n\t\trecordTime := newNow.Add(offset)\n\t\trecord, err := beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\t\"system\": systemRecord.Id,\n\t\t\t\"type\":   \"1m\",\n\t\t\t\"stats\":  string(statsHighJSON),\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trecord.SetRaw(\"created\", recordTime.Format(types.DefaultDateLayout))\n\t\terr = hub.SaveNoValidate(record)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create combined data with high battery\n\tcombinedDataHigh := &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tCpu:     10,\n\t\t\tMemPct:  30,\n\t\t\tDiskPct: 40,\n\t\t\tBattery: [2]uint8{50, 1},\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tAgentVersion: \"0.12.0\",\n\t\t\tCpu:          10,\n\t\t\tMemPct:       30,\n\t\t\tDiskPct:      40,\n\t\t},\n\t}\n\n\t// Update system timestamp to the new time window\n\tsystemRecord.Set(\"updated\", newNow)\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\t// Handle system alerts - should resolve because average battery is now above threshold\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataHigh)\n\trequire.NoError(t, err)\n\n\t// Wait for alert processing\n\ttime.Sleep(20 * time.Millisecond)\n\n\t// Verify alert is resolved (average battery 50% is above threshold 25%)\n\tbatteryAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": batteryAlert.Id})\n\trequire.NoError(t, err)\n\tassert.False(t, batteryAlert.GetBool(\"triggered\"),\n\t\t\"Alert should be resolved when average battery (50%%) is above threshold (25%%) over min period\")\n}\n"
  },
  {
    "path": "internal/alerts/alerts_cache.go",
    "content": "package alerts\n\nimport (\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/store\"\n)\n\n// CachedAlertData represents the relevant fields of an alert record for status checking and updates.\ntype CachedAlertData struct {\n\tId        string\n\tSystemID  string\n\tUserID    string\n\tName      string\n\tValue     float64\n\tTriggered bool\n\tMin       uint8\n\t// Created   types.DateTime\n}\n\nfunc (a *CachedAlertData) PopulateFromRecord(record *core.Record) {\n\ta.Id = record.Id\n\ta.SystemID = record.GetString(\"system\")\n\ta.UserID = record.GetString(\"user\")\n\ta.Name = record.GetString(\"name\")\n\ta.Value = record.GetFloat(\"value\")\n\ta.Triggered = record.GetBool(\"triggered\")\n\ta.Min = uint8(record.GetInt(\"min\"))\n\t// a.Created = record.GetDateTime(\"created\")\n}\n\n// AlertsCache provides an in-memory cache for system alerts.\ntype AlertsCache struct {\n\tapp       core.App\n\tstore     *store.Store[string, *store.Store[string, CachedAlertData]]\n\tpopulated bool\n}\n\n// NewAlertsCache creates a new instance of SystemAlertsCache.\nfunc NewAlertsCache(app core.App) *AlertsCache {\n\tc := AlertsCache{\n\t\tapp:   app,\n\t\tstore: store.New(map[string]*store.Store[string, CachedAlertData]{}),\n\t}\n\treturn c.bindEvents()\n}\n\n// bindEvents sets up event listeners to keep the cache in sync with database changes.\nfunc (c *AlertsCache) bindEvents() *AlertsCache {\n\tc.app.OnRecordAfterUpdateSuccess(\"alerts\").BindFunc(func(e *core.RecordEvent) error {\n\t\t// c.Delete(e.Record.Original()) // this would be needed if the system field on an existing alert was changed, however we don't currently allow that in the UI so we'll leave it commented out\n\t\tc.Update(e.Record)\n\t\treturn e.Next()\n\t})\n\tc.app.OnRecordAfterDeleteSuccess(\"alerts\").BindFunc(func(e *core.RecordEvent) error {\n\t\tc.Delete(e.Record)\n\t\treturn e.Next()\n\t})\n\tc.app.OnRecordAfterCreateSuccess(\"alerts\").BindFunc(func(e *core.RecordEvent) error {\n\t\tc.Update(e.Record)\n\t\treturn e.Next()\n\t})\n\treturn c\n}\n\n// PopulateFromDB clears current entries and loads all alerts from the database into the cache.\nfunc (c *AlertsCache) PopulateFromDB(force bool) error {\n\tif !force && c.populated {\n\t\treturn nil\n\t}\n\trecords, err := c.app.FindAllRecords(\"alerts\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.store.RemoveAll()\n\tfor _, record := range records {\n\t\tc.Update(record)\n\t}\n\tc.populated = true\n\treturn nil\n}\n\n// Update adds or updates an alert record in the cache.\nfunc (c *AlertsCache) Update(record *core.Record) {\n\tsystemID := record.GetString(\"system\")\n\tif systemID == \"\" {\n\t\treturn\n\t}\n\tsystemStore, ok := c.store.GetOk(systemID)\n\tif !ok {\n\t\tsystemStore = store.New(map[string]CachedAlertData{})\n\t\tc.store.Set(systemID, systemStore)\n\t}\n\tvar ca CachedAlertData\n\tca.PopulateFromRecord(record)\n\tsystemStore.Set(record.Id, ca)\n}\n\n// Delete removes an alert record from the cache.\nfunc (c *AlertsCache) Delete(record *core.Record) {\n\tsystemID := record.GetString(\"system\")\n\tif systemID == \"\" {\n\t\treturn\n\t}\n\tif systemStore, ok := c.store.GetOk(systemID); ok {\n\t\tsystemStore.Remove(record.Id)\n\t}\n}\n\n// GetSystemAlerts returns all alerts for the specified system, lazy-loading if necessary.\nfunc (c *AlertsCache) GetSystemAlerts(systemID string) []CachedAlertData {\n\tsystemStore, ok := c.store.GetOk(systemID)\n\tif !ok {\n\t\t// Populate cache for this system\n\t\trecords, err := c.app.FindAllRecords(\"alerts\", dbx.NewExp(\"system={:system}\", dbx.Params{\"system\": systemID}))\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tsystemStore = store.New(map[string]CachedAlertData{})\n\t\tfor _, record := range records {\n\t\t\tvar ca CachedAlertData\n\t\t\tca.PopulateFromRecord(record)\n\t\t\tsystemStore.Set(record.Id, ca)\n\t\t}\n\t\tc.store.Set(systemID, systemStore)\n\t}\n\tall := systemStore.GetAll()\n\talerts := make([]CachedAlertData, 0, len(all))\n\tfor _, alert := range all {\n\t\talerts = append(alerts, alert)\n\t}\n\treturn alerts\n}\n\n// GetAlert returns a specific alert by its ID from the cache.\nfunc (c *AlertsCache) GetAlert(systemID, alertID string) (CachedAlertData, bool) {\n\tif systemStore, ok := c.store.GetOk(systemID); ok {\n\t\treturn systemStore.GetOk(alertID)\n\t}\n\treturn CachedAlertData{}, false\n}\n\n// GetAlertsByName returns all alerts of a specific type for the specified system.\nfunc (c *AlertsCache) GetAlertsByName(systemID, alertName string) []CachedAlertData {\n\tallAlerts := c.GetSystemAlerts(systemID)\n\tvar alerts []CachedAlertData\n\tfor _, record := range allAlerts {\n\t\tif record.Name == alertName {\n\t\t\talerts = append(alerts, record)\n\t\t}\n\t}\n\treturn alerts\n}\n\n// GetAlertsExcludingNames returns all alerts for the specified system excluding the given types.\nfunc (c *AlertsCache) GetAlertsExcludingNames(systemID string, excludedNames ...string) []CachedAlertData {\n\texcludeMap := make(map[string]struct{})\n\tfor _, name := range excludedNames {\n\t\texcludeMap[name] = struct{}{}\n\t}\n\tallAlerts := c.GetSystemAlerts(systemID)\n\tvar alerts []CachedAlertData\n\tfor _, record := range allAlerts {\n\t\tif _, excluded := excludeMap[record.Name]; !excluded {\n\t\t\talerts = append(alerts, record)\n\t\t}\n\t}\n\treturn alerts\n}\n\n// Refresh returns the latest cached copy for an alert snapshot if it still exists.\nfunc (c *AlertsCache) Refresh(alert CachedAlertData) (CachedAlertData, bool) {\n\tif alert.Id == \"\" {\n\t\treturn CachedAlertData{}, false\n\t}\n\treturn c.GetAlert(alert.SystemID, alert.Id)\n}\n"
  },
  {
    "path": "internal/alerts/alerts_cache_test.go",
    "content": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/alerts\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSystemAlertsCachePopulateAndFilter(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 2, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystem1 := systems[0]\n\tsystem2 := systems[1]\n\n\tstatusAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Status\",\n\t\t\"system\": system1.Id,\n\t\t\"user\":   user.Id,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tcpuAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"CPU\",\n\t\t\"system\": system1.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  80,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tmemoryAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Memory\",\n\t\t\"system\": system2.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  90,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tcache := alerts.NewAlertsCache(hub)\n\tcache.PopulateFromDB(false)\n\n\tstatusAlerts := cache.GetAlertsByName(system1.Id, \"Status\")\n\trequire.Len(t, statusAlerts, 1)\n\tassert.Equal(t, statusAlert.Id, statusAlerts[0].Id)\n\n\tnonStatusAlerts := cache.GetAlertsExcludingNames(system1.Id, \"Status\")\n\trequire.Len(t, nonStatusAlerts, 1)\n\tassert.Equal(t, cpuAlert.Id, nonStatusAlerts[0].Id)\n\n\tsystem2Alerts := cache.GetSystemAlerts(system2.Id)\n\trequire.Len(t, system2Alerts, 1)\n\tassert.Equal(t, memoryAlert.Id, system2Alerts[0].Id)\n}\n\nfunc TestSystemAlertsCacheLazyLoadUpdateAndDelete(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\tstatusAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Status\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tcache := alerts.NewAlertsCache(hub)\n\trequire.Len(t, cache.GetSystemAlerts(systemRecord.Id), 1, \"first lookup should lazy-load alerts for the system\")\n\n\tcpuAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"CPU\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  80,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tcache.Update(cpuAlert)\n\n\tnonStatusAlerts := cache.GetAlertsExcludingNames(systemRecord.Id, \"Status\")\n\trequire.Len(t, nonStatusAlerts, 1)\n\tassert.Equal(t, cpuAlert.Id, nonStatusAlerts[0].Id)\n\n\tcache.Delete(statusAlert)\n\tassert.Empty(t, cache.GetAlertsByName(systemRecord.Id, \"Status\"), \"deleted alerts should be removed from the in-memory cache\")\n}\n\nfunc TestSystemAlertsCacheRefreshReturnsLatestCopy(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystem := systems[0]\n\n\talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"Status\",\n\t\t\"system\":    system.Id,\n\t\t\"user\":      user.Id,\n\t\t\"min\":       1,\n\t\t\"triggered\": false,\n\t})\n\trequire.NoError(t, err)\n\n\tcache := alerts.NewAlertsCache(hub)\n\tsnapshot := cache.GetSystemAlerts(system.Id)[0]\n\tassert.False(t, snapshot.Triggered)\n\n\talert.Set(\"triggered\", true)\n\trequire.NoError(t, hub.Save(alert))\n\n\trefreshed, ok := cache.Refresh(snapshot)\n\trequire.True(t, ok)\n\tassert.Equal(t, snapshot.Id, refreshed.Id)\n\tassert.True(t, refreshed.Triggered, \"refresh should return the updated cached value rather than the stale snapshot\")\n\n\trequire.NoError(t, hub.Delete(alert))\n\t_, ok = cache.Refresh(snapshot)\n\tassert.False(t, ok, \"refresh should report false when the cached alert no longer exists\")\n}\n\nfunc TestAlertManagerCacheLifecycle(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystem := systems[0]\n\n\t// Create an alert\n\talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"CPU\",\n\t\t\"system\": system.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  80,\n\t\t\"min\":    1,\n\t})\n\trequire.NoError(t, err)\n\n\tam := hub.AlertManager\n\tcache := am.GetSystemAlertsCache()\n\n\t// Verify it's in cache (it should be since CreateRecord triggers the event)\n\tassert.Len(t, cache.GetSystemAlerts(system.Id), 1)\n\tassert.Equal(t, alert.Id, cache.GetSystemAlerts(system.Id)[0].Id)\n\tassert.EqualValues(t, 80, cache.GetSystemAlerts(system.Id)[0].Value)\n\n\t// Update the alert through PocketBase to trigger events\n\talert.Set(\"value\", 85)\n\trequire.NoError(t, hub.Save(alert))\n\n\t// Check if updated value is reflected (or at least that it's still there)\n\tcachedAlerts := cache.GetSystemAlerts(system.Id)\n\tassert.Len(t, cachedAlerts, 1)\n\tassert.EqualValues(t, 85, cachedAlerts[0].Value)\n\n\t// Delete the alert through PocketBase to trigger events\n\trequire.NoError(t, hub.Delete(alert))\n\n\t// Verify it's removed from cache\n\tassert.Empty(t, cache.GetSystemAlerts(system.Id), \"alert should be removed from cache after PocketBase delete\")\n}\n\n// func TestAlertManagerCacheMovesAlertToNewSystemOnUpdate(t *testing.T) {\n// \thub, user := beszelTests.GetHubWithUser(t)\n// \tdefer hub.Cleanup()\n\n// \tsystems, err := beszelTests.CreateSystems(hub, 2, user.Id, \"up\")\n// \trequire.NoError(t, err)\n// \tsystem1 := systems[0]\n// \tsystem2 := systems[1]\n\n// \talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n// \t\t\"name\":   \"CPU\",\n// \t\t\"system\": system1.Id,\n// \t\t\"user\":   user.Id,\n// \t\t\"value\":  80,\n// \t\t\"min\":    1,\n// \t})\n// \trequire.NoError(t, err)\n\n// \tam := hub.AlertManager\n// \tcache := am.GetSystemAlertsCache()\n\n// \t// Initially in system1 cache\n// \tassert.Len(t, cache.Get(system1.Id), 1)\n// \tassert.Empty(t, cache.Get(system2.Id))\n\n// \t// Move alert to system2\n// \talert.Set(\"system\", system2.Id)\n// \trequire.NoError(t, hub.Save(alert))\n\n// \t// DEBUG: print if it is found\n// \t// fmt.Printf(\"system1 alerts after update: %v\\n\", cache.Get(system1.Id))\n\n// \t// Should be removed from system1 and present in system2\n// \tassert.Empty(t, cache.GetType(system1.Id, \"CPU\"), \"updated alerts should be evicted from the previous system cache\")\n// \trequire.Len(t, cache.Get(system2.Id), 1)\n// \tassert.Equal(t, alert.Id, cache.Get(system2.Id)[0].Id)\n// }\n"
  },
  {
    "path": "internal/alerts/alerts_disk_test.go",
    "content": "//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/internal/entities/system\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/tools/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestDiskAlertExtraFsMultiMinute tests that multi-minute disk alerts correctly use\n// historical per-minute values for extra (non-root) filesystems, not the current live snapshot.\nfunc TestDiskAlertExtraFsMultiMinute(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\t// Disk alert: threshold 80%, min=2 (requires historical averaging)\n\tdiskAlert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Disk\",\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  80, // threshold: 80%\n\t\t\"min\":    2,  // 2 minutes - requires historical averaging\n\t})\n\trequire.NoError(t, err)\n\tassert.False(t, diskAlert.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\tam := hub.GetAlertManager()\n\tnow := time.Now().UTC()\n\n\textraFsHigh := map[string]*system.FsStats{\n\t\t\"/mnt/data\": {DiskTotal: 1000, DiskUsed: 920}, // 92% - above threshold\n\t}\n\n\t// Insert 4 historical records spread over 3 minutes (same pattern as battery tests).\n\t// The oldest record must predate (now - 2min) so the alert time window is valid.\n\trecordTimes := []time.Duration{\n\t\t-180 * time.Second, // 3 min ago - anchors oldest record before alert.time\n\t\t-90 * time.Second,\n\t\t-60 * time.Second,\n\t\t-30 * time.Second,\n\t}\n\n\tfor _, offset := range recordTimes {\n\t\tstats := system.Stats{\n\t\t\tDiskPct: 30, // root disk at 30% - below threshold\n\t\t\tExtraFs: extraFsHigh,\n\t\t}\n\t\tstatsJSON, _ := json.Marshal(stats)\n\n\t\trecordTime := now.Add(offset)\n\t\trecord, err := beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\t\"system\": systemRecord.Id,\n\t\t\t\"type\":   \"1m\",\n\t\t\t\"stats\":  string(statsJSON),\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trecord.SetRaw(\"created\", recordTime.Format(types.DefaultDateLayout))\n\t\terr = hub.SaveNoValidate(record)\n\t\trequire.NoError(t, err)\n\t}\n\n\tcombinedDataHigh := &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tDiskPct: 30,\n\t\t\tExtraFs: extraFsHigh,\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tDiskPct: 30,\n\t\t},\n\t}\n\n\tsystemRecord.Set(\"updated\", now)\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataHigh)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(20 * time.Millisecond)\n\n\tdiskAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": diskAlert.Id})\n\trequire.NoError(t, err)\n\tassert.True(t, diskAlert.GetBool(\"triggered\"),\n\t\t\"Alert SHOULD be triggered when extra disk average (92%%) exceeds threshold (80%%)\")\n\n\t// --- Resolution: extra disk drops to 50%, alert should resolve ---\n\n\textraFsLow := map[string]*system.FsStats{\n\t\t\"/mnt/data\": {DiskTotal: 1000, DiskUsed: 500}, // 50% - below threshold\n\t}\n\n\tnewNow := now.Add(2 * time.Minute)\n\trecordTimesLow := []time.Duration{\n\t\t-180 * time.Second,\n\t\t-90 * time.Second,\n\t\t-60 * time.Second,\n\t\t-30 * time.Second,\n\t}\n\n\tfor _, offset := range recordTimesLow {\n\t\tstats := system.Stats{\n\t\t\tDiskPct: 30,\n\t\t\tExtraFs: extraFsLow,\n\t\t}\n\t\tstatsJSON, _ := json.Marshal(stats)\n\n\t\trecordTime := newNow.Add(offset)\n\t\trecord, err := beszelTests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\t\"system\": systemRecord.Id,\n\t\t\t\"type\":   \"1m\",\n\t\t\t\"stats\":  string(statsJSON),\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trecord.SetRaw(\"created\", recordTime.Format(types.DefaultDateLayout))\n\t\terr = hub.SaveNoValidate(record)\n\t\trequire.NoError(t, err)\n\t}\n\n\tcombinedDataLow := &system.CombinedData{\n\t\tStats: system.Stats{\n\t\t\tDiskPct: 30,\n\t\t\tExtraFs: extraFsLow,\n\t\t},\n\t\tInfo: system.Info{\n\t\t\tDiskPct: 30,\n\t\t},\n\t}\n\n\tsystemRecord.Set(\"updated\", newNow)\n\terr = hub.SaveNoValidate(systemRecord)\n\trequire.NoError(t, err)\n\n\terr = am.HandleSystemAlerts(systemRecord, combinedDataLow)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(20 * time.Millisecond)\n\n\tdiskAlert, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": diskAlert.Id})\n\trequire.NoError(t, err)\n\tassert.False(t, diskAlert.GetBool(\"triggered\"),\n\t\t\"Alert should be resolved when extra disk average (50%%) drops below threshold (80%%)\")\n}\n"
  },
  {
    "path": "internal/alerts/alerts_history.go",
    "content": "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 triggered alert record delete, set matching alert history record to resolved\nfunc resolveHistoryOnAlertDelete(e *core.RecordEvent) error {\n\tif !e.Record.GetBool(\"triggered\") {\n\t\treturn e.Next()\n\t}\n\t_ = resolveAlertHistoryRecord(e.App, e.Record.Id)\n\treturn e.Next()\n}\n\n// On alert record update, update alert history record\nfunc updateHistoryOnAlertUpdate(e *core.RecordEvent) error {\n\toriginal := e.Record.Original()\n\tnew := e.Record\n\n\toriginalTriggered := original.GetBool(\"triggered\")\n\tnewTriggered := new.GetBool(\"triggered\")\n\n\t// no need to update alert history if triggered state has not changed\n\tif originalTriggered == newTriggered {\n\t\treturn e.Next()\n\t}\n\n\t// if new state is triggered, create new alert history record\n\tif newTriggered {\n\t\t_, _ = createAlertHistoryRecord(e.App, new)\n\t\treturn e.Next()\n\t}\n\n\t// if new state is not triggered, check for matching alert history record and set it to resolved\n\t_ = resolveAlertHistoryRecord(e.App, new.Id)\n\treturn e.Next()\n}\n\n// resolveAlertHistoryRecord sets the resolved field to the current time\nfunc resolveAlertHistoryRecord(app core.App, alertRecordID string) error {\n\talertHistoryRecord, err := app.FindFirstRecordByFilter(\"alerts_history\", \"alert_id={:alert_id} && resolved=null\", dbx.Params{\"alert_id\": alertRecordID})\n\tif err != nil || alertHistoryRecord == nil {\n\t\treturn err\n\t}\n\talertHistoryRecord.Set(\"resolved\", time.Now().UTC())\n\terr = app.Save(alertHistoryRecord)\n\tif err != nil {\n\t\tapp.Logger().Error(\"Failed to resolve alert history\", \"err\", err)\n\t}\n\treturn err\n}\n\n// createAlertHistoryRecord creates a new alert history record\nfunc createAlertHistoryRecord(app core.App, alertRecord *core.Record) (alertHistoryRecord *core.Record, err error) {\n\talertHistoryCollection, err := app.FindCachedCollectionByNameOrId(\"alerts_history\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\talertHistoryRecord = core.NewRecord(alertHistoryCollection)\n\talertHistoryRecord.Set(\"alert_id\", alertRecord.Id)\n\talertHistoryRecord.Set(\"user\", alertRecord.GetString(\"user\"))\n\talertHistoryRecord.Set(\"system\", alertRecord.GetString(\"system\"))\n\talertHistoryRecord.Set(\"name\", alertRecord.GetString(\"name\"))\n\talertHistoryRecord.Set(\"value\", alertRecord.GetFloat(\"value\"))\n\terr = app.Save(alertHistoryRecord)\n\tif err != nil {\n\t\tapp.Logger().Error(\"Failed to save alert history\", \"err\", err)\n\t}\n\treturn alertHistoryRecord, err\n}\n"
  },
  {
    "path": "internal/alerts/alerts_quiet_hours_test.go",
    "content": "//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/internal/alerts\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAlertSilencedOneTime(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\tassert.NoError(t, err)\n\tsystem := systems[0]\n\n\t// Create an alert\n\talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"CPU\",\n\t\t\"system\": system.Id,\n\t\t\"user\":   user.Id,\n\t\t\"value\":  80,\n\t\t\"min\":    1,\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a one-time quiet hours window (current time - 1 hour to current time + 1 hour)\n\tnow := time.Now().UTC()\n\tstartTime := now.Add(-1 * time.Hour)\n\tendTime := now.Add(1 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"one-time\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Test that alert is silenced\n\tsilenced := am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.True(t, silenced, \"Alert should be silenced during active one-time window\")\n\n\t// Create a window that has already ended\n\tpastStart := now.Add(-3 * time.Hour)\n\tpastEnd := now.Add(-2 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"one-time\",\n\t\t\"start\":  pastStart,\n\t\t\"end\":    pastEnd,\n\t})\n\tassert.NoError(t, err)\n\n\t// Should still be silenced because of the first window\n\tsilenced = am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.True(t, silenced, \"Alert should still be silenced (past window doesn't affect active window)\")\n\n\t// Clear all windows and create a future window\n\t_, err = hub.DB().NewQuery(\"DELETE FROM quiet_hours\").Execute()\n\tassert.NoError(t, err)\n\n\tfutureStart := now.Add(2 * time.Hour)\n\tfutureEnd := now.Add(3 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"one-time\",\n\t\t\"start\":  futureStart,\n\t\t\"end\":    futureEnd,\n\t})\n\tassert.NoError(t, err)\n\n\t// Alert should NOT be silenced (window hasn't started yet)\n\tsilenced = am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.False(t, silenced, \"Alert should not be silenced (window hasn't started)\")\n\n\t_ = alert\n}\n\nfunc TestAlertSilencedDaily(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\tassert.NoError(t, err)\n\tsystem := systems[0]\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Get current hour and create a window that includes current time\n\tnow := time.Now().UTC()\n\tcurrentHour := now.Hour()\n\tcurrentMin := now.Minute()\n\n\t// Create a window from 1 hour ago to 1 hour from now\n\tstartHour := (currentHour - 1 + 24) % 24\n\tendHour := (currentHour + 1) % 24\n\n\t// Create times with just the hours/minutes we want (date doesn't matter for daily)\n\tstartTime := time.Date(2000, 1, 1, startHour, currentMin, 0, 0, time.UTC)\n\tendTime := time.Date(2000, 1, 1, endHour, currentMin, 0, 0, time.UTC)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"daily\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// Alert should be silenced (current time is within the daily window)\n\tsilenced := am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.True(t, silenced, \"Alert should be silenced during active daily window\")\n\n\t// Clear windows and create one that doesn't include current time\n\t_, err = hub.DB().NewQuery(\"DELETE FROM quiet_hours\").Execute()\n\tassert.NoError(t, err)\n\n\t// Create a window from 6-12 hours from now\n\tfutureStartHour := (currentHour + 6) % 24\n\tfutureEndHour := (currentHour + 12) % 24\n\n\tstartTime = time.Date(2000, 1, 1, futureStartHour, 0, 0, 0, time.UTC)\n\tendTime = time.Date(2000, 1, 1, futureEndHour, 0, 0, 0, time.UTC)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"daily\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// Alert should NOT be silenced\n\tsilenced = am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.False(t, silenced, \"Alert should not be silenced (outside daily window)\")\n}\n\nfunc TestAlertSilencedDailyMidnightCrossing(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\tassert.NoError(t, err)\n\tsystem := systems[0]\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Create a window that crosses midnight: 22:00 - 02:00\n\tstartTime := time.Date(2000, 1, 1, 22, 0, 0, 0, time.UTC)\n\tendTime := time.Date(2000, 1, 1, 2, 0, 0, 0, time.UTC)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"daily\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// Test with a time at 23:00 (should be silenced)\n\t// We can't control the actual current time, but we can verify the logic\n\t// by checking if the window was created correctly\n\twindows, err := hub.FindAllRecords(\"quiet_hours\", dbx.HashExp{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system.Id,\n\t})\n\tassert.NoError(t, err)\n\tassert.Len(t, windows, 1, \"Should have created 1 window\")\n\n\twindow := windows[0]\n\tassert.Equal(t, \"daily\", window.GetString(\"type\"))\n\tassert.Equal(t, 22, window.GetDateTime(\"start\").Time().Hour())\n\tassert.Equal(t, 2, window.GetDateTime(\"end\").Time().Hour())\n}\n\nfunc TestAlertSilencedGlobal(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create multiple systems\n\tsystems, err := beszelTests.CreateSystems(hub, 3, user.Id, \"up\")\n\tassert.NoError(t, err)\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Create a global quiet hours window (no system specified)\n\tnow := time.Now().UTC()\n\tstartTime := now.Add(-1 * time.Hour)\n\tendTime := now.Add(1 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":  user.Id,\n\t\t\"type\":  \"one-time\",\n\t\t\"start\": startTime,\n\t\t\"end\":   endTime,\n\t\t// system field is empty/null for global windows\n\t})\n\tassert.NoError(t, err)\n\n\t// All systems should be silenced\n\tfor _, system := range systems {\n\t\tsilenced := am.IsNotificationSilenced(user.Id, system.Id)\n\t\tassert.True(t, silenced, \"Alert should be silenced for system %s (global window)\", system.Id)\n\t}\n\n\t// Even with a systemID that doesn't exist, should be silenced\n\tsilenced := am.IsNotificationSilenced(user.Id, \"nonexistent-system\")\n\tassert.True(t, silenced, \"Alert should be silenced for any system (global window)\")\n}\n\nfunc TestAlertSilencedSystemSpecific(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create multiple systems\n\tsystems, err := beszelTests.CreateSystems(hub, 2, user.Id, \"up\")\n\tassert.NoError(t, err)\n\tsystem1 := systems[0]\n\tsystem2 := systems[1]\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Create a system-specific quiet hours window for system1 only\n\tnow := time.Now().UTC()\n\tstartTime := now.Add(-1 * time.Hour)\n\tendTime := now.Add(1 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user.Id,\n\t\t\"system\": system1.Id,\n\t\t\"type\":   \"one-time\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// System1 should be silenced\n\tsilenced := am.IsNotificationSilenced(user.Id, system1.Id)\n\tassert.True(t, silenced, \"Alert should be silenced for system1\")\n\n\t// System2 should NOT be silenced\n\tsilenced = am.IsNotificationSilenced(user.Id, system2.Id)\n\tassert.False(t, silenced, \"Alert should not be silenced for system2\")\n}\n\nfunc TestAlertSilencedMultiUser(t *testing.T) {\n\thub, _ := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create two users\n\tuser1, err := beszelTests.CreateUser(hub, \"user1@example.com\", \"password\")\n\tassert.NoError(t, err)\n\n\tuser2, err := beszelTests.CreateUser(hub, \"user2@example.com\", \"password\")\n\tassert.NoError(t, err)\n\n\t// Create a system accessible to both users\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"shared-system\",\n\t\t\"users\": []string{user1.Id, user2.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Create a quiet hours window for user1 only\n\tnow := time.Now().UTC()\n\tstartTime := now.Add(-1 * time.Hour)\n\tendTime := now.Add(1 * time.Hour)\n\n\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\"user\":   user1.Id,\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"one-time\",\n\t\t\"start\":  startTime,\n\t\t\"end\":    endTime,\n\t})\n\tassert.NoError(t, err)\n\n\t// User1 should be silenced\n\tsilenced := am.IsNotificationSilenced(user1.Id, system.Id)\n\tassert.True(t, silenced, \"Alert should be silenced for user1\")\n\n\t// User2 should NOT be silenced\n\tsilenced = am.IsNotificationSilenced(user2.Id, system.Id)\n\tassert.False(t, silenced, \"Alert should not be silenced for user2\")\n}\n\nfunc TestAlertSilencedWithActualAlert(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\t// Create a system\n\t\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\t\tassert.NoError(t, err)\n\t\tsystem := systems[0]\n\n\t\t// Create a status alert\n\t\t_, err = beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Create user settings with email\n\t\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", dbx.Params{\"user\": user.Id})\n\t\tif err != nil || userSettings == nil {\n\t\t\tuserSettings, err = beszelTests.CreateRecord(hub, \"user_settings\", map[string]any{\n\t\t\t\t\"user\": user.Id,\n\t\t\t\t\"settings\": map[string]any{\n\t\t\t\t\t\"emails\": []string{\"test@example.com\"},\n\t\t\t\t},\n\t\t\t})\n\t\t\tassert.NoError(t, err)\n\t\t}\n\n\t\t// Create a quiet hours window\n\t\tnow := time.Now().UTC()\n\t\tstartTime := now.Add(-1 * time.Hour)\n\t\tendTime := now.Add(1 * time.Hour)\n\n\t\t_, err = beszelTests.CreateRecord(hub, \"quiet_hours\", map[string]any{\n\t\t\t\"user\":   user.Id,\n\t\t\t\"system\": system.Id,\n\t\t\t\"type\":   \"one-time\",\n\t\t\t\"start\":  startTime,\n\t\t\t\"end\":    endTime,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Get initial email count\n\t\tinitialEmailCount := hub.TestMailer.TotalSend()\n\n\t\t// Trigger an alert by setting system to down\n\t\tsystem.Set(\"status\", \"down\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\n\t\t// Wait for the alert to be processed (1 minute + buffer)\n\t\ttime.Sleep(time.Second * 75)\n\t\tsynctest.Wait()\n\n\t\t// Check that no email was sent (because alert is silenced)\n\t\tfinalEmailCount := hub.TestMailer.TotalSend()\n\t\tassert.Equal(t, initialEmailCount, finalEmailCount, \"No emails should be sent when alert is silenced\")\n\n\t\t// Clear quiet hours windows\n\t\t_, err = hub.DB().NewQuery(\"DELETE FROM quiet_hours\").Execute()\n\t\tassert.NoError(t, err)\n\n\t\t// Reset system to up, then down again\n\t\tsystem.Set(\"status\", \"up\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\tsystem.Set(\"status\", \"down\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\n\t\t// Wait for the alert to be processed\n\t\ttime.Sleep(time.Second * 75)\n\t\tsynctest.Wait()\n\n\t\t// Now an email should be sent\n\t\tnewEmailCount := hub.TestMailer.TotalSend()\n\t\tassert.Greater(t, newEmailCount, finalEmailCount, \"Email should be sent when not silenced\")\n\t})\n}\n\nfunc TestAlertSilencedNoWindows(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\tassert.NoError(t, err)\n\tsystem := systems[0]\n\n\t// Get alert manager\n\tam := alerts.NewAlertManager(hub)\n\tdefer am.Stop()\n\n\t// Without any quiet hours windows, alert should NOT be silenced\n\tsilenced := am.IsNotificationSilenced(user.Id, system.Id)\n\tassert.False(t, silenced, \"Alert should not be silenced when no windows exist\")\n}\n"
  },
  {
    "path": "internal/alerts/alerts_smart.go",
    "content": "package alerts\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// handleSmartDeviceAlert sends alerts when a SMART device state worsens into WARNING/FAILED.\n// This is automatic and does not require user opt-in.\nfunc (am *AlertManager) handleSmartDeviceAlert(e *core.RecordEvent) error {\n\toldState := e.Record.Original().GetString(\"state\")\n\tnewState := e.Record.GetString(\"state\")\n\n\tif !shouldSendSmartDeviceAlert(oldState, newState) {\n\t\treturn e.Next()\n\t}\n\n\tsystemID := e.Record.GetString(\"system\")\n\tif systemID == \"\" {\n\t\treturn e.Next()\n\t}\n\n\t// Fetch the system record to get the name and users\n\tsystemRecord, err := e.App.FindRecordById(\"systems\", systemID)\n\tif err != nil {\n\t\te.App.Logger().Error(\"Failed to find system for SMART alert\", \"err\", err, \"systemID\", systemID)\n\t\treturn e.Next()\n\t}\n\n\tsystemName := systemRecord.GetString(\"name\")\n\tdeviceName := e.Record.GetString(\"name\")\n\tmodel := e.Record.GetString(\"model\")\n\tstatusLabel := smartStateLabel(newState)\n\n\t// Build alert message\n\ttitle := fmt.Sprintf(\"SMART %s on %s: %s %s\", statusLabel, systemName, deviceName, smartStateEmoji(newState))\n\tvar message string\n\tif model != \"\" {\n\t\tmessage = fmt.Sprintf(\"Disk %s (%s) SMART status changed to %s\", deviceName, model, newState)\n\t} else {\n\t\tmessage = fmt.Sprintf(\"Disk %s SMART status changed to %s\", deviceName, newState)\n\t}\n\n\t// Get users associated with the system\n\tuserIDs := systemRecord.GetStringSlice(\"users\")\n\tif len(userIDs) == 0 {\n\t\treturn e.Next()\n\t}\n\n\t// Send alert to each user\n\tfor _, userID := range userIDs {\n\t\tif err := am.SendAlert(AlertMessageData{\n\t\t\tUserID:   userID,\n\t\t\tSystemID: systemID,\n\t\t\tTitle:    title,\n\t\t\tMessage:  message,\n\t\t\tLink:     am.hub.MakeLink(\"system\", systemID),\n\t\t\tLinkText: \"View \" + systemName,\n\t\t}); err != nil {\n\t\t\te.App.Logger().Error(\"Failed to send SMART alert\", \"err\", err, \"userID\", userID)\n\t\t}\n\t}\n\n\treturn e.Next()\n}\n\nfunc shouldSendSmartDeviceAlert(oldState, newState string) bool {\n\toldSeverity := smartStateSeverity(oldState)\n\tnewSeverity := smartStateSeverity(newState)\n\n\t// Ignore unknown states and recoveries; only alert on worsening transitions\n\t// from known-good/degraded states into WARNING/FAILED.\n\treturn oldSeverity >= 1 && newSeverity > oldSeverity\n}\n\nfunc smartStateSeverity(state string) int {\n\tswitch state {\n\tcase \"PASSED\":\n\t\treturn 1\n\tcase \"WARNING\":\n\t\treturn 2\n\tcase \"FAILED\":\n\t\treturn 3\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc smartStateEmoji(state string) string {\n\tswitch state {\n\tcase \"WARNING\":\n\t\treturn \"\\U0001F7E0\"\n\tdefault:\n\t\treturn \"\\U0001F534\"\n\t}\n}\n\nfunc smartStateLabel(state string) string {\n\tswitch state {\n\tcase \"FAILED\":\n\t\treturn \"failure\"\n\tdefault:\n\t\treturn strings.ToLower(state)\n\t}\n}\n"
  },
  {
    "path": "internal/alerts/alerts_smart_test.go",
    "content": "//go:build testing\n\npackage alerts_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSmartDeviceAlert(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system for the user\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a smart_device with state PASSED\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/sda\",\n\t\t\"model\":  \"Samsung SSD 970 EVO\",\n\t\t\"state\":  \"PASSED\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Verify no emails sent initially\n\tassert.Zero(t, hub.TestMailer.TotalSend(), \"should have 0 emails sent initially\")\n\n\t// Re-fetch the record so PocketBase can properly track original values\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\t// Update the smart device state to FAILED\n\tsmartDevice.Set(\"state\", \"FAILED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\t// Wait for the alert to be processed\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify that an email was sent\n\tassert.EqualValues(t, 1, hub.TestMailer.TotalSend(), \"should have 1 email sent after state changed to FAILED\")\n\n\t// Check the email content\n\tlastMessage := hub.TestMailer.LastMessage()\n\tassert.Contains(t, lastMessage.Subject, \"SMART failure on test-system\")\n\tassert.Contains(t, lastMessage.Subject, \"/dev/sda\")\n\tassert.Contains(t, lastMessage.Text, \"Samsung SSD 970 EVO\")\n\tassert.Contains(t, lastMessage.Text, \"FAILED\")\n}\n\nfunc TestSmartDeviceAlertPassedToWarning(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/mmcblk0\",\n\t\t\"model\":  \"eMMC\",\n\t\t\"state\":  \"PASSED\",\n\t})\n\tassert.NoError(t, err)\n\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\tsmartDevice.Set(\"state\", \"WARNING\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tassert.EqualValues(t, 1, hub.TestMailer.TotalSend(), \"should have 1 email sent after state changed to WARNING\")\n\tlastMessage := hub.TestMailer.LastMessage()\n\tassert.Contains(t, lastMessage.Subject, \"SMART warning on test-system\")\n\tassert.Contains(t, lastMessage.Text, \"WARNING\")\n}\n\nfunc TestSmartDeviceAlertWarningToFailed(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/mmcblk0\",\n\t\t\"model\":  \"eMMC\",\n\t\t\"state\":  \"WARNING\",\n\t})\n\tassert.NoError(t, err)\n\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\tsmartDevice.Set(\"state\", \"FAILED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tassert.EqualValues(t, 1, hub.TestMailer.TotalSend(), \"should have 1 email sent after state changed from WARNING to FAILED\")\n\tlastMessage := hub.TestMailer.LastMessage()\n\tassert.Contains(t, lastMessage.Subject, \"SMART failure on test-system\")\n\tassert.Contains(t, lastMessage.Text, \"FAILED\")\n}\n\nfunc TestSmartDeviceAlertNoAlertOnNonPassedToFailed(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system for the user\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a smart_device with state UNKNOWN\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/sda\",\n\t\t\"model\":  \"Samsung SSD 970 EVO\",\n\t\t\"state\":  \"UNKNOWN\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Re-fetch the record so PocketBase can properly track original values\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\t// Update the state from UNKNOWN to FAILED - should NOT trigger alert.\n\t// We only alert from known healthy/degraded states.\n\tsmartDevice.Set(\"state\", \"FAILED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify no email was sent (only PASSED -> FAILED triggers alert)\n\tassert.Zero(t, hub.TestMailer.TotalSend(), \"should have 0 emails when changing from UNKNOWN to FAILED\")\n\n\t// Re-fetch the record again\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\t// Update state from FAILED to PASSED - should NOT trigger alert\n\tsmartDevice.Set(\"state\", \"PASSED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify no email was sent\n\tassert.Zero(t, hub.TestMailer.TotalSend(), \"should have 0 emails when changing from FAILED to PASSED\")\n}\n\nfunc TestSmartDeviceAlertMultipleUsers(t *testing.T) {\n\thub, user1 := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a second user\n\tuser2, err := beszelTests.CreateUser(hub, \"test2@example.com\", \"password\")\n\tassert.NoError(t, err)\n\n\t// Create user settings for the second user\n\t_, err = beszelTests.CreateRecord(hub, \"user_settings\", map[string]any{\n\t\t\"user\":     user2.Id,\n\t\t\"settings\": `{\"emails\":[\"test2@example.com\"],\"webhooks\":[]}`,\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a system with both users\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"shared-system\",\n\t\t\"users\": []string{user1.Id, user2.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a smart_device with state PASSED\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/nvme0n1\",\n\t\t\"model\":  \"WD Black SN850\",\n\t\t\"state\":  \"PASSED\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Re-fetch the record so PocketBase can properly track original values\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\t// Update the smart device state to FAILED\n\tsmartDevice.Set(\"state\", \"FAILED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify that two emails were sent (one for each user)\n\tassert.EqualValues(t, 2, hub.TestMailer.TotalSend(), \"should have 2 emails sent for 2 users\")\n}\n\nfunc TestSmartDeviceAlertWithoutModel(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a system for the user\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a smart_device with state PASSED but no model\n\tsmartDevice, err := beszelTests.CreateRecord(hub, \"smart_devices\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"name\":   \"/dev/sdb\",\n\t\t\"state\":  \"PASSED\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Re-fetch the record so PocketBase can properly track original values\n\tsmartDevice, err = hub.FindRecordById(\"smart_devices\", smartDevice.Id)\n\tassert.NoError(t, err)\n\n\t// Update the smart device state to FAILED\n\tsmartDevice.Set(\"state\", \"FAILED\")\n\terr = hub.Save(smartDevice)\n\tassert.NoError(t, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// Verify that an email was sent\n\tassert.EqualValues(t, 1, hub.TestMailer.TotalSend(), \"should have 1 email sent\")\n\n\t// Check that the email doesn't have empty parentheses for missing model\n\tlastMessage := hub.TestMailer.LastMessage()\n\tassert.NotContains(t, lastMessage.Text, \"()\", \"should not have empty parentheses for missing model\")\n\tassert.Contains(t, lastMessage.Text, \"/dev/sdb\")\n}\n"
  },
  {
    "path": "internal/alerts/alerts_status.go",
    "content": "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\tsystemName string\n\talertData  CachedAlertData\n\texpireTime time.Time\n\ttimer      *time.Timer\n}\n\n// Stop cancels all pending status alert timers.\nfunc (am *AlertManager) Stop() {\n\tam.stopOnce.Do(func() {\n\t\tam.pendingAlerts.Range(func(key, value any) bool {\n\t\t\tinfo := value.(*alertInfo)\n\t\t\tif info.timer != nil {\n\t\t\t\tinfo.timer.Stop()\n\t\t\t}\n\t\t\tam.pendingAlerts.Delete(key)\n\t\t\treturn true\n\t\t})\n\t})\n}\n\n// HandleStatusAlerts manages the logic when system status changes.\nfunc (am *AlertManager) HandleStatusAlerts(newStatus string, systemRecord *core.Record) error {\n\tif newStatus != \"up\" && newStatus != \"down\" {\n\t\treturn nil\n\t}\n\n\talerts := am.alertsCache.GetAlertsByName(systemRecord.Id, \"Status\")\n\tif len(alerts) == 0 {\n\t\treturn nil\n\t}\n\n\tsystemName := systemRecord.GetString(\"name\")\n\tif newStatus == \"down\" {\n\t\tam.handleSystemDown(systemName, alerts)\n\t} else {\n\t\tam.handleSystemUp(systemName, alerts)\n\t}\n\treturn nil\n}\n\n// handleSystemDown manages the logic when a system status changes to \"down\". It schedules pending alerts for each alert record.\nfunc (am *AlertManager) handleSystemDown(systemName string, alerts []CachedAlertData) {\n\tfor _, alertData := range alerts {\n\t\tmin := max(1, int(alertData.Min))\n\t\tam.schedulePendingStatusAlert(systemName, alertData, time.Duration(min)*time.Minute)\n\t}\n}\n\n// schedulePendingStatusAlert sets up a timer to send a \"down\" alert after the specified delay if the system is still down.\n// It returns true if the alert was scheduled, or false if an alert was already pending for the given alert record.\nfunc (am *AlertManager) schedulePendingStatusAlert(systemName string, alertData CachedAlertData, delay time.Duration) bool {\n\talert := &alertInfo{\n\t\tsystemName: systemName,\n\t\talertData:  alertData,\n\t\texpireTime: time.Now().Add(delay),\n\t}\n\n\tstoredAlert, loaded := am.pendingAlerts.LoadOrStore(alertData.Id, alert)\n\tif loaded {\n\t\treturn false\n\t}\n\n\tstored := storedAlert.(*alertInfo)\n\tstored.timer = time.AfterFunc(time.Until(stored.expireTime), func() {\n\t\tam.processPendingAlert(alertData.Id)\n\t})\n\treturn true\n}\n\n// handleSystemUp manages the logic when a system status changes to \"up\".\n// It cancels any pending alerts and sends \"up\" alerts.\nfunc (am *AlertManager) handleSystemUp(systemName string, alerts []CachedAlertData) {\n\tfor _, alertData := range alerts {\n\t\t// If alert exists for record, delete and continue (down alert not sent)\n\t\tif am.cancelPendingAlert(alertData.Id) {\n\t\t\tcontinue\n\t\t}\n\t\tif !alertData.Triggered {\n\t\t\tcontinue\n\t\t}\n\t\tif err := am.sendStatusAlert(\"up\", systemName, alertData); err != nil {\n\t\t\tam.hub.Logger().Error(\"Failed to send alert\", \"err\", err)\n\t\t}\n\t}\n}\n\n// cancelPendingAlert stops the timer and removes the pending alert for the given alert ID. Returns true if a pending alert was found and cancelled.\nfunc (am *AlertManager) cancelPendingAlert(alertID string) bool {\n\tvalue, loaded := am.pendingAlerts.LoadAndDelete(alertID)\n\tif !loaded {\n\t\treturn false\n\t}\n\n\tinfo := value.(*alertInfo)\n\tif info.timer != nil {\n\t\tinfo.timer.Stop()\n\t}\n\treturn true\n}\n\n// processPendingAlert sends a \"down\" alert if the pending alert has expired and the system is still down.\nfunc (am *AlertManager) processPendingAlert(alertID string) {\n\tvalue, loaded := am.pendingAlerts.LoadAndDelete(alertID)\n\tif !loaded {\n\t\treturn\n\t}\n\n\tinfo := value.(*alertInfo)\n\trefreshedAlertData, ok := am.alertsCache.Refresh(info.alertData)\n\tif !ok || refreshedAlertData.Triggered {\n\t\treturn\n\t}\n\tif err := am.sendStatusAlert(\"down\", info.systemName, refreshedAlertData); err != nil {\n\t\tam.hub.Logger().Error(\"Failed to send alert\", \"err\", err)\n\t}\n}\n\n// sendStatusAlert sends a status alert (\"up\" or \"down\") to the users associated with the alert records.\nfunc (am *AlertManager) sendStatusAlert(alertStatus string, systemName string, alertData CachedAlertData) error {\n\t// Update trigger state for alert record before sending alert\n\ttriggered := alertStatus == \"down\"\n\tif err := am.setAlertTriggered(alertData, triggered); err != nil {\n\t\treturn err\n\t}\n\n\tvar emoji string\n\tif alertStatus == \"up\" {\n\t\temoji = \"\\u2705\" // Green checkmark emoji\n\t} else {\n\t\temoji = \"\\U0001F534\" // Red alert emoji\n\t}\n\n\ttitle := fmt.Sprintf(\"Connection to %s is %s %v\", systemName, alertStatus, emoji)\n\tmessage := strings.TrimSuffix(title, emoji)\n\n\t// Get system ID for the link\n\tsystemID := alertData.SystemID\n\n\treturn am.SendAlert(AlertMessageData{\n\t\tUserID:   alertData.UserID,\n\t\tSystemID: systemID,\n\t\tTitle:    title,\n\t\tMessage:  message,\n\t\tLink:     am.hub.MakeLink(\"system\", systemID),\n\t\tLinkText: \"View \" + systemName,\n\t})\n}\n\n// resolveStatusAlerts resolves any triggered status alerts that weren't resolved\n// when system came up (https://github.com/henrygd/beszel/issues/1052).\nfunc resolveStatusAlerts(app core.App) error {\n\tdb := app.DB()\n\t// Find all active status alerts where the system is actually up\n\tvar alertIds []string\n\terr := db.NewQuery(`\n\t\tSELECT a.id \n\t\tFROM alerts a\n\t\tJOIN systems s ON a.system = s.id\n\t\tWHERE a.name = 'Status' \n\t\tAND a.triggered = true\n\t\tAND s.status = 'up'\n\t`).Column(&alertIds)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// resolve all matching alert records\n\tfor _, alertId := range alertIds {\n\t\talert, err := app.FindRecordById(\"alerts\", alertId)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\talert.Set(\"triggered\", false)\n\t\terr = app.Save(alert)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// restorePendingStatusAlerts re-queues untriggered status alerts for systems that\n// are still down after a hub restart. This rebuilds the lost in-memory timer state.\nfunc (am *AlertManager) restorePendingStatusAlerts() error {\n\ttype pendingStatusAlert struct {\n\t\tAlertID    string `db:\"alert_id\"`\n\t\tSystemID   string `db:\"system_id\"`\n\t\tSystemName string `db:\"system_name\"`\n\t}\n\n\tvar pending []pendingStatusAlert\n\terr := am.hub.DB().NewQuery(`\n\t\tSELECT a.id AS alert_id, a.system AS system_id, s.name AS system_name\n\t\tFROM alerts a\n\t\tJOIN systems s ON a.system = s.id\n\t\tWHERE a.name = 'Status'\n\t\tAND a.triggered = false\n\t\tAND s.status = 'down'\n\t`).All(&pending)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Make sure cache is populated before trying to restore pending alerts\n\t_ = am.alertsCache.PopulateFromDB(false)\n\n\tfor _, item := range pending {\n\t\talertData, ok := am.alertsCache.GetAlert(item.SystemID, item.AlertID)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tmin := max(1, int(alertData.Min))\n\t\tam.schedulePendingStatusAlert(item.SystemName, alertData, time.Duration(min)*time.Minute)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/alerts/alerts_status_test.go",
    "content": "//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/internal/alerts\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc setStatusAlertEmail(t *testing.T, hub core.App, userID, email string) {\n\tt.Helper()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": userID})\n\trequire.NoError(t, err)\n\n\tuserSettings.Set(\"settings\", map[string]any{\n\t\t\"emails\":   []string{email},\n\t\t\"webhooks\": []string{},\n\t})\n\trequire.NoError(t, hub.Save(userSettings))\n}\n\nfunc TestStatusAlerts(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\tsystems, err := beszelTests.CreateSystems(hub, 4, user.Id, \"paused\")\n\t\tassert.NoError(t, err)\n\n\t\tvar alerts []*core.Record\n\t\tfor i, system := range systems {\n\t\t\talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\t\"name\":   \"Status\",\n\t\t\t\t\"system\": system.Id,\n\t\t\t\t\"user\":   user.Id,\n\t\t\t\t\"min\":    i + 1,\n\t\t\t})\n\t\t\tassert.NoError(t, err)\n\t\t\talerts = append(alerts, alert)\n\t\t}\n\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tfor _, alert := range alerts {\n\t\t\tassert.False(t, alert.GetBool(\"triggered\"), \"Alert should not be triggered immediately\")\n\t\t}\n\t\tif hub.TestMailer.TotalSend() != 0 {\n\t\t\tassert.Zero(t, hub.TestMailer.TotalSend(), \"Expected 0 messages, got %d\", hub.TestMailer.TotalSend())\n\t\t}\n\t\tfor _, system := range systems {\n\t\t\tassert.EqualValues(t, \"paused\", system.GetString(\"status\"), \"System should be paused\")\n\t\t}\n\t\tfor _, system := range systems {\n\t\t\tsystem.Set(\"status\", \"up\")\n\t\t\terr = hub.SaveNoValidate(system)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t\tassert.EqualValues(t, 0, hub.GetPendingAlertsCount(), \"should have 0 alerts in the pendingAlerts map\")\n\t\tfor _, system := range systems {\n\t\t\tsystem.Set(\"status\", \"down\")\n\t\t\terr = hub.SaveNoValidate(system)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\t// after 30 seconds, should have 4 alerts in the pendingAlerts map, no triggered alerts\n\t\ttime.Sleep(time.Second * 30)\n\t\tassert.EqualValues(t, 4, hub.GetPendingAlertsCount(), \"should have 4 alerts in the pendingAlerts map\")\n\t\ttriggeredCount, err := hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 0, triggeredCount, \"should have 0 alert triggered\")\n\t\tassert.EqualValues(t, 0, hub.TestMailer.TotalSend(), \"should have 0 messages sent\")\n\t\t// after 1:30 seconds, should have 1 triggered alert and 3 pending alerts\n\t\ttime.Sleep(time.Second * 60)\n\t\tassert.EqualValues(t, 3, hub.GetPendingAlertsCount(), \"should have 3 alerts in the pendingAlerts map\")\n\t\ttriggeredCount, err = hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, triggeredCount, \"should have 1 alert triggered\")\n\t\tassert.EqualValues(t, 1, hub.TestMailer.TotalSend(), \"should have 1 messages sent\")\n\t\t// after 2:30 seconds, should have 2 triggered alerts and 2 pending alerts\n\t\ttime.Sleep(time.Second * 60)\n\t\tassert.EqualValues(t, 2, hub.GetPendingAlertsCount(), \"should have 2 alerts in the pendingAlerts map\")\n\t\ttriggeredCount, err = hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 2, triggeredCount, \"should have 2 alert triggered\")\n\t\tassert.EqualValues(t, 2, hub.TestMailer.TotalSend(), \"should have 2 messages sent\")\n\t\t// now we will bring the remaning systems back up\n\t\tfor _, system := range systems {\n\t\t\tsystem.Set(\"status\", \"up\")\n\t\t\terr = hub.SaveNoValidate(system)\n\t\t\tassert.NoError(t, err)\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t\t// should have 0 alerts in the pendingAlerts map and 0 alerts triggered\n\t\tassert.EqualValues(t, 0, hub.GetPendingAlertsCount(), \"should have 0 alerts in the pendingAlerts map\")\n\t\ttriggeredCount, err = hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true})\n\t\tassert.NoError(t, err)\n\t\tassert.Zero(t, triggeredCount, \"should have 0 alert triggered\")\n\t\t// 4 messages sent, 2 down alerts and 2 up alerts for first 2 systems\n\t\tassert.EqualValues(t, 4, hub.TestMailer.TotalSend(), \"should have 4 messages sent\")\n\t})\n}\nfunc TestStatusAlertRecoveryBeforeDeadline(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Ensure user settings have an email\n\tuserSettings, _ := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\thub.Save(userSettings)\n\n\t// Initial email count\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\n\tsystemCollection, _ := hub.FindCollectionByNameOrId(\"systems\")\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\thub.Save(system)\n\n\talertCollection, _ := hub.FindCollectionByNameOrId(\"alerts\")\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", false)\n\talert.Set(\"min\", 1)\n\thub.Save(alert)\n\n\tam := hub.AlertManager\n\n\t// 1. System goes down\n\tam.HandleStatusAlerts(\"down\", system)\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"Alert should be scheduled\")\n\n\t// 2. System goes up BEFORE delay expires\n\t// Triggering HandleStatusAlerts(\"up\") SHOULD NOT send an alert.\n\tam.HandleStatusAlerts(\"up\", system)\n\n\tassert.Equal(t, 0, am.GetPendingAlertsCount(), \"Alert should be canceled if system recovers before delay expires\")\n\n\t// Verify that NO email was sent.\n\tassert.Equal(t, initialEmailCount, hub.TestMailer.TotalSend(), \"Recovery notification should not be sent if system never went down\")\n\n}\n\nfunc TestStatusAlertNormalRecovery(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Ensure user settings have an email\n\tuserSettings, _ := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\thub.Save(userSettings)\n\n\tsystemCollection, _ := hub.FindCollectionByNameOrId(\"systems\")\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\thub.Save(system)\n\n\talertCollection, _ := hub.FindCollectionByNameOrId(\"alerts\")\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", true) // System was confirmed DOWN\n\thub.Save(alert)\n\n\tam := hub.AlertManager\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\n\t// System goes up\n\tam.HandleStatusAlerts(\"up\", system)\n\n\t// Verify that an email WAS sent (normal recovery).\n\tassert.Equal(t, initialEmailCount+1, hub.TestMailer.TotalSend(), \"Recovery notification should be sent if system was triggered as down\")\n\n}\n\nfunc TestHandleStatusAlertsDoesNotSendRecoveryWhileDownIsOnlyPending(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\trequire.NoError(t, err)\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\trequire.NoError(t, hub.Save(userSettings))\n\n\tsystemCollection, err := hub.FindCollectionByNameOrId(\"systems\")\n\trequire.NoError(t, err)\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\trequire.NoError(t, hub.Save(system))\n\n\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\trequire.NoError(t, err)\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", false)\n\talert.Set(\"min\", 1)\n\trequire.NoError(t, hub.Save(alert))\n\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"down transition should register a pending alert immediately\")\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"up\", system))\n\tassert.Zero(t, am.GetPendingAlertsCount(), \"recovery should cancel the pending down alert\")\n\tassert.Equal(t, initialEmailCount, hub.TestMailer.TotalSend(), \"recovery notification should not be sent before a down alert triggers\")\n\n\talertRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\trequire.NoError(t, err)\n\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"alert should remain untriggered when downtime never matured\")\n}\n\nfunc TestStatusAlertTimerCancellationPreventsBoundaryDelivery(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\t\trequire.NoError(t, err)\n\t\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\t\trequire.NoError(t, hub.Save(userSettings))\n\n\t\tsystemCollection, err := hub.FindCollectionByNameOrId(\"systems\")\n\t\trequire.NoError(t, err)\n\t\tsystem := core.NewRecord(systemCollection)\n\t\tsystem.Set(\"name\", \"test-system\")\n\t\tsystem.Set(\"status\", \"up\")\n\t\tsystem.Set(\"host\", \"127.0.0.1\")\n\t\tsystem.Set(\"users\", []string{user.Id})\n\t\trequire.NoError(t, hub.Save(system))\n\n\t\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\t\trequire.NoError(t, err)\n\t\talert := core.NewRecord(alertCollection)\n\t\talert.Set(\"user\", user.Id)\n\t\talert.Set(\"system\", system.Id)\n\t\talert.Set(\"name\", \"Status\")\n\t\talert.Set(\"triggered\", false)\n\t\talert.Set(\"min\", 1)\n\t\trequire.NoError(t, hub.Save(alert))\n\n\t\tinitialEmailCount := hub.TestMailer.TotalSend()\n\t\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\t\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\t\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"down transition should register a pending alert immediately\")\n\t\trequire.True(t, am.ResetPendingAlertTimer(alert.Id, 25*time.Millisecond), \"test should shorten the pending alert timer\")\n\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\trequire.NoError(t, am.HandleStatusAlerts(\"up\", system))\n\t\tassert.Zero(t, am.GetPendingAlertsCount(), \"recovery should remove the pending alert before the timer callback runs\")\n\n\t\ttime.Sleep(40 * time.Millisecond)\n\t\tassert.Equal(t, initialEmailCount, hub.TestMailer.TotalSend(), \"timer callback should not deliver after recovery cancels the pending alert\")\n\n\t\talertRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"alert should remain untriggered when cancellation wins the timer race\")\n\n\t\ttime.Sleep(time.Minute)\n\t\tsynctest.Wait()\n\t})\n}\n\nfunc TestStatusAlertDownFiresAfterDelayExpires(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\trequire.NoError(t, err)\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\trequire.NoError(t, hub.Save(userSettings))\n\n\tsystemCollection, err := hub.FindCollectionByNameOrId(\"systems\")\n\trequire.NoError(t, err)\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\trequire.NoError(t, hub.Save(system))\n\n\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\trequire.NoError(t, err)\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", false)\n\talert.Set(\"min\", 1)\n\trequire.NoError(t, hub.Save(alert))\n\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"alert should be pending after system goes down\")\n\n\t// Expire the pending alert and process it\n\tam.ForceExpirePendingAlerts()\n\tprocessed, err := am.ProcessPendingAlerts()\n\trequire.NoError(t, err)\n\tassert.Len(t, processed, 1, \"one alert should have been processed\")\n\tassert.Equal(t, 0, am.GetPendingAlertsCount(), \"pending alert should be consumed after processing\")\n\n\t// Verify down email was sent\n\tassert.Equal(t, initialEmailCount+1, hub.TestMailer.TotalSend(), \"down notification should be sent after delay expires\")\n\n\t// Verify triggered flag is set in the DB\n\talertRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\trequire.NoError(t, err)\n\tassert.True(t, alertRecord.GetBool(\"triggered\"), \"alert should be marked triggered after downtime matures\")\n}\n\nfunc TestStatusAlertMultipleUsersRespectDifferentMinutes(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user1 := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\tsetStatusAlertEmail(t, hub, user1.Id, \"user1@example.com\")\n\n\t\tuser2, err := beszelTests.CreateUser(hub, \"user2@example.com\", \"password\")\n\t\trequire.NoError(t, err)\n\t\t_, err = beszelTests.CreateRecord(hub, \"user_settings\", map[string]any{\n\t\t\t\"user\": user2.Id,\n\t\t\t\"settings\": map[string]any{\n\t\t\t\t\"emails\":   []string{\"user2@example.com\"},\n\t\t\t\t\"webhooks\": []string{},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"shared-system\",\n\t\t\t\"users\": []string{user1.Id, user2.Id},\n\t\t\t\"host\":  \"127.0.0.1\",\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tsystem.Set(\"status\", \"up\")\n\t\trequire.NoError(t, hub.SaveNoValidate(system))\n\n\t\talertUser1, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user1.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\talertUser2, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user2.Id,\n\t\t\t\"min\":    2,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tsystem.Set(\"status\", \"down\")\n\t\trequire.NoError(t, hub.SaveNoValidate(system))\n\n\t\tassert.Equal(t, 2, hub.GetPendingAlertsCount(), \"both user alerts should be pending after the system goes down\")\n\n\t\ttime.Sleep(59 * time.Second)\n\t\tsynctest.Wait()\n\t\tassert.Zero(t, hub.TestMailer.TotalSend(), \"no messages should be sent before the earliest alert minute elapses\")\n\n\t\ttime.Sleep(2 * time.Second)\n\t\tsynctest.Wait()\n\n\t\tmessages := hub.TestMailer.Messages()\n\t\trequire.Len(t, messages, 1, \"only the first user's alert should send after one minute\")\n\t\trequire.Len(t, messages[0].To, 1)\n\t\tassert.Equal(t, \"user1@example.com\", messages[0].To[0].Address)\n\t\tassert.Contains(t, messages[0].Subject, \"Connection to shared-system is down\")\n\t\tassert.Equal(t, 1, hub.GetPendingAlertsCount(), \"the later user alert should still be pending\")\n\n\t\ttime.Sleep(58 * time.Second)\n\t\tsynctest.Wait()\n\t\tassert.Equal(t, 1, hub.TestMailer.TotalSend(), \"the second user's alert should still be waiting before two minutes\")\n\n\t\ttime.Sleep(2 * time.Second)\n\t\tsynctest.Wait()\n\n\t\tmessages = hub.TestMailer.Messages()\n\t\trequire.Len(t, messages, 2, \"both users should eventually receive their own status alert\")\n\t\trequire.Len(t, messages[1].To, 1)\n\t\tassert.Equal(t, \"user2@example.com\", messages[1].To[0].Address)\n\t\tassert.Contains(t, messages[1].Subject, \"Connection to shared-system is down\")\n\t\tassert.Zero(t, hub.GetPendingAlertsCount(), \"all pending alerts should be consumed after both timers fire\")\n\n\t\talertUser1, err = hub.FindRecordById(\"alerts\", alertUser1.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, alertUser1.GetBool(\"triggered\"), \"user1 alert should be marked triggered after delivery\")\n\n\t\talertUser2, err = hub.FindRecordById(\"alerts\", alertUser2.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, alertUser2.GetBool(\"triggered\"), \"user2 alert should be marked triggered after delivery\")\n\t})\n}\n\nfunc TestStatusAlertMultipleUsersRecoveryBetweenMinutesOnlyAlertsEarlierUser(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user1 := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\tsetStatusAlertEmail(t, hub, user1.Id, \"user1@example.com\")\n\n\t\tuser2, err := beszelTests.CreateUser(hub, \"user2@example.com\", \"password\")\n\t\trequire.NoError(t, err)\n\t\t_, err = beszelTests.CreateRecord(hub, \"user_settings\", map[string]any{\n\t\t\t\"user\": user2.Id,\n\t\t\t\"settings\": map[string]any{\n\t\t\t\t\"emails\":   []string{\"user2@example.com\"},\n\t\t\t\t\"webhooks\": []string{},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"shared-system\",\n\t\t\t\"users\": []string{user1.Id, user2.Id},\n\t\t\t\"host\":  \"127.0.0.1\",\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tsystem.Set(\"status\", \"up\")\n\t\trequire.NoError(t, hub.SaveNoValidate(system))\n\n\t\talertUser1, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user1.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\talertUser2, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user2.Id,\n\t\t\t\"min\":    2,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\tsystem.Set(\"status\", \"down\")\n\t\trequire.NoError(t, hub.SaveNoValidate(system))\n\n\t\ttime.Sleep(61 * time.Second)\n\t\tsynctest.Wait()\n\n\t\tmessages := hub.TestMailer.Messages()\n\t\trequire.Len(t, messages, 1, \"the first user's down alert should send before recovery\")\n\t\trequire.Len(t, messages[0].To, 1)\n\t\tassert.Equal(t, \"user1@example.com\", messages[0].To[0].Address)\n\t\tassert.Contains(t, messages[0].Subject, \"Connection to shared-system is down\")\n\t\tassert.Equal(t, 1, hub.GetPendingAlertsCount(), \"the second user's alert should still be pending\")\n\n\t\tsystem.Set(\"status\", \"up\")\n\t\trequire.NoError(t, hub.SaveNoValidate(system))\n\n\t\ttime.Sleep(time.Second)\n\t\tsynctest.Wait()\n\n\t\tmessages = hub.TestMailer.Messages()\n\t\trequire.Len(t, messages, 2, \"recovery should notify only the user whose down alert had already triggered\")\n\t\tfor _, message := range messages {\n\t\t\trequire.Len(t, message.To, 1)\n\t\t\tassert.Equal(t, \"user1@example.com\", message.To[0].Address)\n\t\t}\n\t\tassert.Contains(t, messages[1].Subject, \"Connection to shared-system is up\")\n\t\tassert.Zero(t, hub.GetPendingAlertsCount(), \"recovery should cancel the later user's pending alert\")\n\n\t\ttime.Sleep(61 * time.Second)\n\t\tsynctest.Wait()\n\n\t\tmessages = hub.TestMailer.Messages()\n\t\trequire.Len(t, messages, 2, \"user2 should never receive a down alert once recovery cancels the pending timer\")\n\n\t\talertUser1, err = hub.FindRecordById(\"alerts\", alertUser1.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, alertUser1.GetBool(\"triggered\"), \"user1 alert should be cleared after recovery\")\n\n\t\talertUser2, err = hub.FindRecordById(\"alerts\", alertUser2.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, alertUser2.GetBool(\"triggered\"), \"user2 alert should remain untriggered because it never fired\")\n\t})\n}\n\nfunc TestStatusAlertDuplicateDownCallIsIdempotent(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\trequire.NoError(t, err)\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\trequire.NoError(t, hub.Save(userSettings))\n\n\tsystemCollection, err := hub.FindCollectionByNameOrId(\"systems\")\n\trequire.NoError(t, err)\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\trequire.NoError(t, hub.Save(system))\n\n\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\trequire.NoError(t, err)\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", false)\n\talert.Set(\"min\", 5)\n\trequire.NoError(t, hub.Save(alert))\n\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"repeated down calls should not schedule duplicate pending alerts\")\n}\n\nfunc TestStatusAlertNoAlertRecord(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystemCollection, err := hub.FindCollectionByNameOrId(\"systems\")\n\trequire.NoError(t, err)\n\tsystem := core.NewRecord(systemCollection)\n\tsystem.Set(\"name\", \"test-system\")\n\tsystem.Set(\"status\", \"up\")\n\tsystem.Set(\"host\", \"127.0.0.1\")\n\tsystem.Set(\"users\", []string{user.Id})\n\trequire.NoError(t, hub.Save(system))\n\n\t// No Status alert record created for this system\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"down\", system))\n\tassert.Equal(t, 0, am.GetPendingAlertsCount(), \"no pending alert when no alert record exists\")\n\n\trequire.NoError(t, am.HandleStatusAlerts(\"up\", system))\n\tassert.Equal(t, initialEmailCount, hub.TestMailer.TotalSend(), \"no email when no alert record exists\")\n}\n\nfunc TestRestorePendingStatusAlertsRequeuesDownSystemsAfterRestart(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\trequire.NoError(t, err)\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\trequire.NoError(t, hub.Save(userSettings))\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"down\")\n\trequire.NoError(t, err)\n\tsystem := systems[0]\n\n\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\trequire.NoError(t, err)\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", false)\n\talert.Set(\"min\", 1)\n\trequire.NoError(t, hub.Save(alert))\n\n\tinitialEmailCount := hub.TestMailer.TotalSend()\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\n\trequire.NoError(t, am.RestorePendingStatusAlerts())\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"startup restore should requeue a pending down alert for a system still marked down\")\n\n\tam.ForceExpirePendingAlerts()\n\tprocessed, err := am.ProcessPendingAlerts()\n\trequire.NoError(t, err)\n\tassert.Len(t, processed, 1, \"restored pending alert should be processable after the delay expires\")\n\tassert.Equal(t, initialEmailCount+1, hub.TestMailer.TotalSend(), \"restored pending alert should send the down notification\")\n\n\talertRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\trequire.NoError(t, err)\n\tassert.True(t, alertRecord.GetBool(\"triggered\"), \"restored pending alert should mark the alert as triggered once delivered\")\n}\n\nfunc TestRestorePendingStatusAlertsSkipsNonDownOrAlreadyTriggeredAlerts(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystemsDown, err := beszelTests.CreateSystems(hub, 2, user.Id, \"down\")\n\trequire.NoError(t, err)\n\tsystemDownPending := systemsDown[0]\n\tsystemDownTriggered := systemsDown[1]\n\n\tsystemUp, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"up-system\",\n\t\t\"users\":  []string{user.Id},\n\t\t\"host\":   \"127.0.0.2\",\n\t\t\"status\": \"up\",\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"Status\",\n\t\t\"system\":    systemDownPending.Id,\n\t\t\"user\":      user.Id,\n\t\t\"min\":       1,\n\t\t\"triggered\": false,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"Status\",\n\t\t\"system\":    systemUp.Id,\n\t\t\"user\":      user.Id,\n\t\t\"min\":       1,\n\t\t\"triggered\": false,\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"Status\",\n\t\t\"system\":    systemDownTriggered.Id,\n\t\t\"user\":      user.Id,\n\t\t\"min\":       1,\n\t\t\"triggered\": true,\n\t})\n\trequire.NoError(t, err)\n\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\trequire.NoError(t, am.RestorePendingStatusAlerts())\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"only untriggered alerts for currently down systems should be restored\")\n}\n\nfunc TestRestorePendingStatusAlertsIsIdempotent(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"down\")\n\trequire.NoError(t, err)\n\tsystem := systems[0]\n\n\t_, err = beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"Status\",\n\t\t\"system\":    system.Id,\n\t\t\"user\":      user.Id,\n\t\t\"min\":       1,\n\t\t\"triggered\": false,\n\t})\n\trequire.NoError(t, err)\n\n\tam := alerts.NewTestAlertManagerWithoutWorker(hub)\n\trequire.NoError(t, am.RestorePendingStatusAlerts())\n\trequire.NoError(t, am.RestorePendingStatusAlerts())\n\n\tassert.Equal(t, 1, am.GetPendingAlertsCount(), \"restoring twice should not create duplicate pending alerts\")\n\tam.ForceExpirePendingAlerts()\n\tprocessed, err := am.ProcessPendingAlerts()\n\trequire.NoError(t, err)\n\tassert.Len(t, processed, 1, \"restored alert should still be processable exactly once\")\n\tassert.Zero(t, am.GetPendingAlertsCount(), \"processing the restored alert should empty the pending map\")\n}\n\nfunc TestResolveStatusAlertsFixesStaleTriggered(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// CreateSystems uses SaveNoValidate after initial save to bypass the\n\t// onRecordCreate hook that forces status = \"pending\".\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystem := systems[0]\n\n\talertCollection, err := hub.FindCollectionByNameOrId(\"alerts\")\n\trequire.NoError(t, err)\n\talert := core.NewRecord(alertCollection)\n\talert.Set(\"user\", user.Id)\n\talert.Set(\"system\", system.Id)\n\talert.Set(\"name\", \"Status\")\n\talert.Set(\"triggered\", true) // Stale: system is up but alert still says triggered\n\trequire.NoError(t, hub.Save(alert))\n\n\t// resolveStatusAlerts should clear the stale triggered flag\n\trequire.NoError(t, alerts.ResolveStatusAlerts(hub))\n\n\talertRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\trequire.NoError(t, err)\n\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"stale triggered flag should be cleared when system is up\")\n}\nfunc TestResolveStatusAlerts(t *testing.T) {\n\thub, user := beszelTests.GetHubWithUser(t)\n\tdefer hub.Cleanup()\n\n\t// Create a systemUp\n\tsystemUp, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"users\":  []string{user.Id},\n\t\t\"host\":   \"127.0.0.1\",\n\t\t\"status\": \"up\",\n\t})\n\tassert.NoError(t, err)\n\n\tsystemDown, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system-2\",\n\t\t\"users\":  []string{user.Id},\n\t\t\"host\":   \"127.0.0.2\",\n\t\t\"status\": \"up\",\n\t})\n\tassert.NoError(t, err)\n\n\t// Create a status alertUp for the system\n\talertUp, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Status\",\n\t\t\"system\": systemUp.Id,\n\t\t\"user\":   user.Id,\n\t\t\"min\":    1,\n\t})\n\tassert.NoError(t, err)\n\n\talertDown, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   \"Status\",\n\t\t\"system\": systemDown.Id,\n\t\t\"user\":   user.Id,\n\t\t\"min\":    1,\n\t})\n\tassert.NoError(t, err)\n\n\t// Verify alert is not triggered initially\n\tassert.False(t, alertUp.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\t// Set the system to 'up' (this should not trigger the alert)\n\tsystemUp.Set(\"status\", \"up\")\n\terr = hub.SaveNoValidate(systemUp)\n\tassert.NoError(t, err)\n\n\tsystemDown.Set(\"status\", \"down\")\n\terr = hub.SaveNoValidate(systemDown)\n\tassert.NoError(t, err)\n\n\t// Wait a moment for any processing\n\ttime.Sleep(10 * time.Millisecond)\n\n\t// Verify alertUp is still not triggered after setting system to up\n\talertUp, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": alertUp.Id})\n\tassert.NoError(t, err)\n\tassert.False(t, alertUp.GetBool(\"triggered\"), \"Alert should not be triggered when system is up\")\n\n\t// Manually set both alerts triggered to true\n\talertUp.Set(\"triggered\", true)\n\terr = hub.SaveNoValidate(alertUp)\n\tassert.NoError(t, err)\n\talertDown.Set(\"triggered\", true)\n\terr = hub.SaveNoValidate(alertDown)\n\tassert.NoError(t, err)\n\n\t// Verify we have exactly one alert with triggered true\n\ttriggeredCount, err := hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true})\n\tassert.NoError(t, err)\n\tassert.EqualValues(t, 2, triggeredCount, \"Should have exactly two alerts with triggered true\")\n\n\t// Verify the specific alertUp is triggered\n\talertUp, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": alertUp.Id})\n\tassert.NoError(t, err)\n\tassert.True(t, alertUp.GetBool(\"triggered\"), \"Alert should be triggered\")\n\n\t// Verify we have two unresolved alert history records\n\talertHistoryCount, err := hub.CountRecords(\"alerts_history\", dbx.HashExp{\"resolved\": \"\"})\n\tassert.NoError(t, err)\n\tassert.EqualValues(t, 2, alertHistoryCount, \"Should have exactly two unresolved alert history records\")\n\n\terr = alerts.ResolveStatusAlerts(hub)\n\tassert.NoError(t, err)\n\n\t// Verify alertUp is not triggered after resolving\n\talertUp, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": alertUp.Id})\n\tassert.NoError(t, err)\n\tassert.False(t, alertUp.GetBool(\"triggered\"), \"Alert should not be triggered after resolving\")\n\t// Verify alertDown is still triggered\n\talertDown, err = hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": alertDown.Id})\n\tassert.NoError(t, err)\n\tassert.True(t, alertDown.GetBool(\"triggered\"), \"Alert should still be triggered after resolving\")\n\n\t// Verify we have one unresolved alert history record\n\talertHistoryCount, err = hub.CountRecords(\"alerts_history\", dbx.HashExp{\"resolved\": \"\"})\n\tassert.NoError(t, err)\n\tassert.EqualValues(t, 1, alertHistoryCount, \"Should have exactly one unresolved alert history record\")\n\n}\n\nfunc TestAlertsHistoryStatus(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\t// Create a system\n\t\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\t\tassert.NoError(t, err)\n\t\tsystem := systems[0]\n\n\t\t// Create a status alertRecord for the system\n\t\talertRecord, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Verify alert is not triggered initially\n\t\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\t\t// Set the system to 'down' (this should trigger the alert)\n\t\tsystem.Set(\"status\", \"down\")\n\t\terr = hub.Save(system)\n\t\tassert.NoError(t, err)\n\n\t\ttime.Sleep(time.Second * 30)\n\t\tsynctest.Wait()\n\n\t\talertFresh, _ := hub.FindRecordById(\"alerts\", alertRecord.Id)\n\t\tassert.False(t, alertFresh.GetBool(\"triggered\"), \"Alert should not be triggered after 30 seconds\")\n\n\t\ttime.Sleep(time.Minute)\n\t\tsynctest.Wait()\n\n\t\t// Verify alert is triggered after setting system to down\n\t\talertFresh, err = hub.FindRecordById(\"alerts\", alertRecord.Id)\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, alertFresh.GetBool(\"triggered\"), \"Alert should be triggered after one minute\")\n\n\t\t// Verify we have one unresolved alert history record\n\t\talertHistoryCount, err := hub.CountRecords(\"alerts_history\", dbx.HashExp{\"resolved\": \"\"})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, alertHistoryCount, \"Should have exactly one unresolved alert history record\")\n\n\t\t// Set the system back to 'up' (this should resolve the alert)\n\t\tsystem.Set(\"status\", \"up\")\n\t\terr = hub.Save(system)\n\t\tassert.NoError(t, err)\n\n\t\ttime.Sleep(time.Second)\n\t\tsynctest.Wait()\n\n\t\t// Verify alert is not triggered after setting system back to up\n\t\talertFresh, err = hub.FindRecordById(\"alerts\", alertRecord.Id)\n\t\tassert.NoError(t, err)\n\t\tassert.False(t, alertFresh.GetBool(\"triggered\"), \"Alert should not be triggered after system recovers\")\n\n\t\t// Verify the alert history record is resolved\n\t\talertHistoryCount, err = hub.CountRecords(\"alerts_history\", dbx.HashExp{\"resolved\": \"\"})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 0, alertHistoryCount, \"Should have no unresolved alert history records\")\n\t})\n}\n\nfunc TestStatusAlertClearedBeforeSend(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\t// Create a system\n\t\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\t\tassert.NoError(t, err)\n\t\tsystem := systems[0]\n\n\t\t// Ensure user settings have an email\n\t\tuserSettings, _ := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\t\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\t\thub.Save(userSettings)\n\n\t\t// Initial email count\n\t\tinitialEmailCount := hub.TestMailer.TotalSend()\n\n\t\t// Create a status alertRecord for the system\n\t\talertRecord, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Verify alert is not triggered initially\n\t\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\t\t// Set the system to 'down' (this should trigger the alert)\n\t\tsystem.Set(\"status\", \"down\")\n\t\terr = hub.Save(system)\n\t\tassert.NoError(t, err)\n\n\t\ttime.Sleep(time.Second * 30)\n\t\tsynctest.Wait()\n\n\t\t// Set system back up to clear the pending alert before it triggers\n\t\tsystem.Set(\"status\", \"up\")\n\t\terr = hub.Save(system)\n\t\tassert.NoError(t, err)\n\n\t\ttime.Sleep(time.Minute)\n\t\tsynctest.Wait()\n\n\t\t// Verify that we have not sent any emails since the system recovered before the alert triggered\n\t\tassert.Equal(t, initialEmailCount, hub.TestMailer.TotalSend(), \"No email should be sent if system recovers before alert triggers\")\n\n\t\t// Verify alert is not triggered after setting system back to up\n\t\talertFresh, err := hub.FindRecordById(\"alerts\", alertRecord.Id)\n\t\tassert.NoError(t, err)\n\t\tassert.False(t, alertFresh.GetBool(\"triggered\"), \"Alert should not be triggered after system recovers\")\n\n\t\t// Verify that no alert history record was created since the alert never triggered\n\t\talertHistoryCount, err := hub.CountRecords(\"alerts_history\")\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 0, alertHistoryCount, \"Should have no unresolved alert history records since alert never triggered\")\n\t})\n}\n"
  },
  {
    "path": "internal/alerts/alerts_system.go",
    "content": "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/system\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/types\"\n)\n\nfunc (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {\n\talerts := am.alertsCache.GetAlertsExcludingNames(systemRecord.Id, \"Status\")\n\tif len(alerts) == 0 {\n\t\treturn nil\n\t}\n\n\tvar validAlerts []SystemAlertData\n\tnow := systemRecord.GetDateTime(\"updated\").Time().UTC()\n\toldestTime := now\n\n\tfor _, alertData := range alerts {\n\t\tname := alertData.Name\n\t\tvar val float64\n\t\tunit := \"%\"\n\n\t\tswitch name {\n\t\tcase \"CPU\":\n\t\t\tval = data.Info.Cpu\n\t\tcase \"Memory\":\n\t\t\tval = data.Info.MemPct\n\t\tcase \"Bandwidth\":\n\t\t\tval = float64(data.Info.BandwidthBytes) / (1024 * 1024)\n\t\t\tunit = \" MB/s\"\n\t\tcase \"Disk\":\n\t\t\tmaxUsedPct := data.Info.DiskPct\n\t\t\tfor _, fs := range data.Stats.ExtraFs {\n\t\t\t\tusedPct := fs.DiskUsed / fs.DiskTotal * 100\n\t\t\t\tif usedPct > maxUsedPct {\n\t\t\t\t\tmaxUsedPct = usedPct\n\t\t\t\t}\n\t\t\t}\n\t\t\tval = maxUsedPct\n\t\tcase \"Temperature\":\n\t\t\tif data.Info.DashboardTemp < 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tval = data.Info.DashboardTemp\n\t\t\tunit = \"°C\"\n\t\tcase \"LoadAvg1\":\n\t\t\tval = data.Info.LoadAvg[0]\n\t\t\tunit = \"\"\n\t\tcase \"LoadAvg5\":\n\t\t\tval = data.Info.LoadAvg[1]\n\t\t\tunit = \"\"\n\t\tcase \"LoadAvg15\":\n\t\t\tval = data.Info.LoadAvg[2]\n\t\t\tunit = \"\"\n\t\tcase \"GPU\":\n\t\t\tval = data.Info.GpuPct\n\t\tcase \"Battery\":\n\t\t\tif data.Stats.Battery[0] == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tval = float64(data.Stats.Battery[0])\n\t\t}\n\n\t\ttriggered := alertData.Triggered\n\t\tthreshold := alertData.Value\n\n\t\t// Battery alert has inverted logic: trigger when value is BELOW threshold\n\t\tlowAlert := isLowAlert(name)\n\n\t\t// CONTINUE\n\t\t// For normal alerts: IF not triggered and curValue <= threshold, OR triggered and curValue > threshold\n\t\t// For low alerts (Battery): IF not triggered and curValue >= threshold, OR triggered and curValue < threshold\n\t\tif lowAlert {\n\t\t\tif (!triggered && val >= threshold) || (triggered && val < threshold) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\tif (!triggered && val <= threshold) || (triggered && val > threshold) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tmin := max(1, alertData.Min)\n\n\t\talert := SystemAlertData{\n\t\t\tsystemRecord: systemRecord,\n\t\t\talertData:    alertData,\n\t\t\tname:         name,\n\t\t\tunit:         unit,\n\t\t\tval:          val,\n\t\t\tthreshold:    threshold,\n\t\t\ttriggered:    triggered,\n\t\t\tmin:          min,\n\t\t}\n\n\t\t// send alert immediately if min is 1 - no need to sum up values.\n\t\tif min == 1 {\n\t\t\tif lowAlert {\n\t\t\t\talert.triggered = val < threshold\n\t\t\t} else {\n\t\t\t\talert.triggered = val > threshold\n\t\t\t}\n\t\t\tgo am.sendSystemAlert(alert)\n\t\t\tcontinue\n\t\t}\n\n\t\talert.time = now.Add(-time.Duration(min) * time.Minute)\n\t\tif alert.time.Before(oldestTime) {\n\t\t\toldestTime = alert.time\n\t\t}\n\n\t\tvalidAlerts = append(validAlerts, alert)\n\t}\n\n\tsystemStats := []struct {\n\t\tStats   []byte         `db:\"stats\"`\n\t\tCreated types.DateTime `db:\"created\"`\n\t}{}\n\n\terr := am.hub.DB().\n\t\tSelect(\"stats\", \"created\").\n\t\tFrom(\"system_stats\").\n\t\tWhere(dbx.NewExp(\n\t\t\t\"system={:system} AND type='1m' AND created > {:created}\",\n\t\t\tdbx.Params{\n\t\t\t\t\"system\": systemRecord.Id,\n\t\t\t\t// subtract some time to give us a bit of buffer\n\t\t\t\t\"created\": oldestTime.Add(-time.Second * 90),\n\t\t\t},\n\t\t)).\n\t\tOrderBy(\"created\").\n\t\tAll(&systemStats)\n\tif err != nil || len(systemStats) == 0 {\n\t\treturn err\n\t}\n\n\t// get oldest record creation time from first record in the slice\n\toldestRecordTime := systemStats[0].Created.Time()\n\t// log.Println(\"oldestRecordTime\", oldestRecordTime.String())\n\n\t// Filter validAlerts to keep only those with time newer than oldestRecord\n\tfilteredAlerts := make([]SystemAlertData, 0, len(validAlerts))\n\tfor _, alert := range validAlerts {\n\t\tif alert.time.After(oldestRecordTime) {\n\t\t\tfilteredAlerts = append(filteredAlerts, alert)\n\t\t}\n\t}\n\tvalidAlerts = filteredAlerts\n\n\tif len(validAlerts) == 0 {\n\t\t// log.Println(\"no valid alerts found\")\n\t\treturn nil\n\t}\n\n\tvar stats SystemAlertStats\n\n\t// we can skip the latest systemStats record since it's the current value\n\tfor i := range systemStats {\n\t\tstat := systemStats[i]\n\t\t// subtract 10 seconds to give a small time buffer\n\t\tsystemStatsCreation := stat.Created.Time().Add(-time.Second * 10)\n\t\tif err := json.Unmarshal(stat.Stats, &stats); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// log.Println(\"stats\", stats)\n\t\tfor j := range validAlerts {\n\t\t\talert := &validAlerts[j]\n\t\t\t// reset alert val on first iteration\n\t\t\tif i == 0 {\n\t\t\t\talert.val = 0\n\t\t\t}\n\t\t\t// continue if system_stats is older than alert time range\n\t\t\tif systemStatsCreation.Before(alert.time) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// add to alert value\n\t\t\tswitch alert.name {\n\t\t\tcase \"CPU\":\n\t\t\t\talert.val += stats.Cpu\n\t\t\tcase \"Memory\":\n\t\t\t\talert.val += stats.Mem\n\t\t\tcase \"Bandwidth\":\n\t\t\t\talert.val += float64(stats.Bandwidth[0]+stats.Bandwidth[1]) / (1024 * 1024)\n\t\t\tcase \"Disk\":\n\t\t\t\tif alert.mapSums == nil {\n\t\t\t\t\talert.mapSums = make(map[string]float32, len(stats.ExtraFs)+1)\n\t\t\t\t}\n\t\t\t\t// add root disk\n\t\t\t\tif _, ok := alert.mapSums[\"root\"]; !ok {\n\t\t\t\t\talert.mapSums[\"root\"] = 0.0\n\t\t\t\t}\n\t\t\t\talert.mapSums[\"root\"] += float32(stats.Disk)\n\t\t\t\t// add extra disks from historical record\n\t\t\t\tfor key, fs := range stats.ExtraFs {\n\t\t\t\t\tif fs.DiskTotal > 0 {\n\t\t\t\t\t\tif _, ok := alert.mapSums[key]; !ok {\n\t\t\t\t\t\t\talert.mapSums[key] = 0.0\n\t\t\t\t\t\t}\n\t\t\t\t\t\talert.mapSums[key] += float32(fs.DiskUsed / fs.DiskTotal * 100)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"Temperature\":\n\t\t\t\tif alert.mapSums == nil {\n\t\t\t\t\talert.mapSums = make(map[string]float32, len(stats.Temperatures))\n\t\t\t\t}\n\t\t\t\tfor key, temp := range stats.Temperatures {\n\t\t\t\t\tif _, ok := alert.mapSums[key]; !ok {\n\t\t\t\t\t\talert.mapSums[key] = float32(0)\n\t\t\t\t\t}\n\t\t\t\t\talert.mapSums[key] += temp\n\t\t\t\t}\n\t\t\tcase \"LoadAvg1\":\n\t\t\t\talert.val += stats.LoadAvg[0]\n\t\t\tcase \"LoadAvg5\":\n\t\t\t\talert.val += stats.LoadAvg[1]\n\t\t\tcase \"LoadAvg15\":\n\t\t\t\talert.val += stats.LoadAvg[2]\n\t\t\tcase \"GPU\":\n\t\t\t\tif len(stats.GPU) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmaxUsage := 0.0\n\t\t\t\tfor _, gpu := range stats.GPU {\n\t\t\t\t\tif gpu.Usage > maxUsage {\n\t\t\t\t\t\tmaxUsage = gpu.Usage\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\talert.val += maxUsage\n\t\t\tcase \"Battery\":\n\t\t\t\talert.val += float64(stats.Battery[0])\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\talert.count++\n\t\t}\n\t}\n\t// sum up vals for each alert\n\tfor _, alert := range validAlerts {\n\t\tswitch alert.name {\n\t\tcase \"Disk\":\n\t\t\tmaxPct := float32(0)\n\t\t\tfor key, value := range alert.mapSums {\n\t\t\t\tsumPct := float32(value)\n\t\t\t\tif sumPct > maxPct {\n\t\t\t\t\tmaxPct = sumPct\n\t\t\t\t\talert.descriptor = fmt.Sprintf(\"Usage of %s\", key)\n\t\t\t\t}\n\t\t\t}\n\t\t\talert.val = float64(maxPct / float32(alert.count))\n\t\tcase \"Temperature\":\n\t\t\tmaxTemp := float32(0)\n\t\t\tfor key, value := range alert.mapSums {\n\t\t\t\tsumTemp := float32(value) / float32(alert.count)\n\t\t\t\tif sumTemp > maxTemp {\n\t\t\t\t\tmaxTemp = sumTemp\n\t\t\t\t\talert.descriptor = fmt.Sprintf(\"Highest sensor %s\", key)\n\t\t\t\t}\n\t\t\t}\n\t\t\talert.val = float64(maxTemp)\n\t\tdefault:\n\t\t\talert.val = alert.val / float64(alert.count)\n\t\t}\n\t\tminCount := float32(alert.min) / 1.2\n\t\t// log.Println(\"alert\", alert.name, \"val\", alert.val, \"threshold\", alert.threshold, \"triggered\", alert.triggered)\n\t\t// log.Printf(\"%s: val %f | count %d | min-count %f | threshold %f\\n\", alert.name, alert.val, alert.count, minCount, alert.threshold)\n\t\t// pass through alert if count is greater than or equal to minCount\n\t\tif float32(alert.count) >= minCount {\n\t\t\t// Battery alert has inverted logic: trigger when value is BELOW threshold\n\t\t\tlowAlert := isLowAlert(alert.name)\n\t\t\tif lowAlert {\n\t\t\t\tif !alert.triggered && alert.val < alert.threshold {\n\t\t\t\t\talert.triggered = true\n\t\t\t\t\tgo am.sendSystemAlert(alert)\n\t\t\t\t} else if alert.triggered && alert.val >= alert.threshold {\n\t\t\t\t\talert.triggered = false\n\t\t\t\t\tgo am.sendSystemAlert(alert)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif !alert.triggered && alert.val > alert.threshold {\n\t\t\t\t\talert.triggered = true\n\t\t\t\t\tgo am.sendSystemAlert(alert)\n\t\t\t\t} else if alert.triggered && alert.val <= alert.threshold {\n\t\t\t\t\talert.triggered = false\n\t\t\t\t\tgo am.sendSystemAlert(alert)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (am *AlertManager) sendSystemAlert(alert SystemAlertData) {\n\t// log.Printf(\"Sending alert %s: val %f | count %d | threshold %f\\n\", alert.name, alert.val, alert.count, alert.threshold)\n\tsystemName := alert.systemRecord.GetString(\"name\")\n\n\t// change Disk to Disk usage\n\tif alert.name == \"Disk\" {\n\t\talert.name += \" usage\"\n\t}\n\t// format LoadAvg5 and LoadAvg15\n\tif after, ok := strings.CutPrefix(alert.name, \"LoadAvg\"); ok {\n\t\talert.name = after + \"m Load\"\n\t}\n\n\t// make title alert name lowercase if not CPU or GPU\n\ttitleAlertName := alert.name\n\tif titleAlertName != \"CPU\" && titleAlertName != \"GPU\" {\n\t\ttitleAlertName = strings.ToLower(titleAlertName)\n\t}\n\n\tvar subject string\n\tlowAlert := isLowAlert(alert.name)\n\tif alert.triggered {\n\t\tif lowAlert {\n\t\t\tsubject = fmt.Sprintf(\"%s %s below threshold\", systemName, titleAlertName)\n\t\t} else {\n\t\t\tsubject = fmt.Sprintf(\"%s %s above threshold\", systemName, titleAlertName)\n\t\t}\n\t} else {\n\t\tif lowAlert {\n\t\t\tsubject = fmt.Sprintf(\"%s %s above threshold\", systemName, titleAlertName)\n\t\t} else {\n\t\t\tsubject = fmt.Sprintf(\"%s %s below threshold\", systemName, titleAlertName)\n\t\t}\n\t}\n\tminutesLabel := \"minute\"\n\tif alert.min > 1 {\n\t\tminutesLabel += \"s\"\n\t}\n\tif alert.descriptor == \"\" {\n\t\talert.descriptor = alert.name\n\t}\n\tbody := fmt.Sprintf(\"%s averaged %.2f%s for the previous %v %s.\", alert.descriptor, alert.val, alert.unit, alert.min, minutesLabel)\n\n\tif err := am.setAlertTriggered(alert.alertData, alert.triggered); err != nil {\n\t\t// app.Logger().Error(\"failed to save alert record\", \"err\", err)\n\t\treturn\n\t}\n\tam.SendAlert(AlertMessageData{\n\t\tUserID:   alert.alertData.UserID,\n\t\tSystemID: alert.systemRecord.Id,\n\t\tTitle:    subject,\n\t\tMessage:  body,\n\t\tLink:     am.hub.MakeLink(\"system\", alert.systemRecord.Id),\n\t\tLinkText: \"View \" + systemName,\n\t})\n}\n\nfunc isLowAlert(name string) bool {\n\treturn name == \"Battery\"\n}\n"
  },
  {
    "path": "internal/alerts/alerts_system_test.go",
    "content": "//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/internal/entities/system\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype systemAlertValueSetter[T any] func(info *system.Info, stats *system.Stats, value T)\n\ntype systemAlertTestFixture struct {\n\thub     *beszelTests.TestHub\n\talertID string\n\tsubmit  func(*system.CombinedData) error\n}\n\nfunc createCombinedData[T any](value T, setValue systemAlertValueSetter[T]) *system.CombinedData {\n\tvar data system.CombinedData\n\tsetValue(&data.Info, &data.Stats, value)\n\treturn &data\n}\n\nfunc newSystemAlertTestFixture(t *testing.T, alertName string, min int, threshold float64) *systemAlertTestFixture {\n\tt.Helper()\n\n\thub, user := beszelTests.GetHubWithUser(t)\n\n\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\trequire.NoError(t, err)\n\tsystemRecord := systems[0]\n\n\tsysManagerSystem, err := hub.GetSystemManager().GetSystemFromStore(systemRecord.Id)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sysManagerSystem)\n\tsysManagerSystem.StopUpdater()\n\n\tuserSettings, err := hub.FindFirstRecordByFilter(\"user_settings\", \"user={:user}\", map[string]any{\"user\": user.Id})\n\trequire.NoError(t, err)\n\tuserSettings.Set(\"settings\", `{\"emails\":[\"test@example.com\"],\"webhooks\":[]}`)\n\trequire.NoError(t, hub.Save(userSettings))\n\n\talertRecord, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":   alertName,\n\t\t\"system\": systemRecord.Id,\n\t\t\"user\":   user.Id,\n\t\t\"min\":    min,\n\t\t\"value\":  threshold,\n\t})\n\trequire.NoError(t, err)\n\n\tassert.False(t, alertRecord.GetBool(\"triggered\"), \"Alert should not be triggered initially\")\n\n\talertsCache := hub.GetAlertManager().GetSystemAlertsCache()\n\tcachedAlerts := alertsCache.GetAlertsExcludingNames(systemRecord.Id, \"Status\")\n\tassert.Len(t, cachedAlerts, 1, \"Alert should be in cache\")\n\n\treturn &systemAlertTestFixture{\n\t\thub:     hub,\n\t\talertID: alertRecord.Id,\n\t\tsubmit: func(data *system.CombinedData) error {\n\t\t\t_, err := sysManagerSystem.CreateRecords(data)\n\t\t\treturn err\n\t\t},\n\t}\n}\n\nfunc (fixture *systemAlertTestFixture) cleanup() {\n\tfixture.hub.Cleanup()\n}\n\nfunc submitValue[T any](fixture *systemAlertTestFixture, t *testing.T, value T, setValue systemAlertValueSetter[T]) {\n\tt.Helper()\n\trequire.NoError(t, fixture.submit(createCombinedData(value, setValue)))\n}\n\nfunc (fixture *systemAlertTestFixture) assertTriggered(t *testing.T, triggered bool, message string) {\n\tt.Helper()\n\n\talertRecord, err := fixture.hub.FindRecordById(\"alerts\", fixture.alertID)\n\trequire.NoError(t, err)\n\tassert.Equal(t, triggered, alertRecord.GetBool(\"triggered\"), message)\n}\n\nfunc waitForSystemAlert(d time.Duration) {\n\ttime.Sleep(d)\n\tsynctest.Wait()\n}\n\nfunc testOneMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, setValue systemAlertValueSetter[T], triggerValue, resolveValue T) {\n\tt.Helper()\n\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tfixture := newSystemAlertTestFixture(t, alertName, 1, threshold)\n\t\tdefer fixture.cleanup()\n\n\t\tsubmitValue(fixture, t, triggerValue, setValue)\n\t\twaitForSystemAlert(time.Second)\n\n\t\tfixture.assertTriggered(t, true, \"Alert should be triggered\")\n\t\tassert.Equal(t, 1, fixture.hub.TestMailer.TotalSend(), \"An email should have been sent\")\n\n\t\tsubmitValue(fixture, t, resolveValue, setValue)\n\t\twaitForSystemAlert(time.Second)\n\n\t\tfixture.assertTriggered(t, false, \"Alert should be untriggered\")\n\t\tassert.Equal(t, 2, fixture.hub.TestMailer.TotalSend(), \"A second email should have been sent for untriggering the alert\")\n\n\t\twaitForSystemAlert(time.Minute)\n\t})\n}\n\nfunc testMultiMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, min int, setValue systemAlertValueSetter[T], baselineValue, triggerValue, resolveValue T) {\n\tt.Helper()\n\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tfixture := newSystemAlertTestFixture(t, alertName, min, threshold)\n\t\tdefer fixture.cleanup()\n\n\t\tsubmitValue(fixture, t, baselineValue, setValue)\n\t\twaitForSystemAlert(time.Minute + time.Second)\n\t\tfixture.assertTriggered(t, false, \"Alert should not be triggered yet\")\n\n\t\tsubmitValue(fixture, t, triggerValue, setValue)\n\t\twaitForSystemAlert(time.Minute)\n\t\tfixture.assertTriggered(t, false, \"Alert should not be triggered until the history window is full\")\n\n\t\tsubmitValue(fixture, t, triggerValue, setValue)\n\t\twaitForSystemAlert(time.Second)\n\t\tfixture.assertTriggered(t, true, \"Alert should be triggered\")\n\t\tassert.Equal(t, 1, fixture.hub.TestMailer.TotalSend(), \"An email should have been sent\")\n\n\t\tsubmitValue(fixture, t, resolveValue, setValue)\n\t\twaitForSystemAlert(time.Second)\n\t\tfixture.assertTriggered(t, false, \"Alert should be untriggered\")\n\t\tassert.Equal(t, 2, fixture.hub.TestMailer.TotalSend(), \"A second email should have been sent for untriggering the alert\")\n\t})\n}\n\nfunc setCPUAlertValue(info *system.Info, stats *system.Stats, value float64) {\n\tinfo.Cpu = value\n\tstats.Cpu = value\n}\n\nfunc setMemoryAlertValue(info *system.Info, stats *system.Stats, value float64) {\n\tinfo.MemPct = value\n\tstats.MemPct = value\n}\n\nfunc setDiskAlertValue(info *system.Info, stats *system.Stats, value float64) {\n\tinfo.DiskPct = value\n\tstats.DiskPct = value\n}\n\nfunc setBandwidthAlertValue(info *system.Info, stats *system.Stats, value [2]uint64) {\n\tinfo.BandwidthBytes = value[0] + value[1]\n\tstats.Bandwidth = value\n}\n\nfunc megabytesToBytes(mb uint64) uint64 {\n\treturn mb * 1024 * 1024\n}\n\nfunc setGPUAlertValue(info *system.Info, stats *system.Stats, value float64) {\n\tinfo.GpuPct = value\n\tstats.GPUData = map[string]system.GPUData{\n\t\t\"GPU0\": {Usage: value},\n\t}\n}\n\nfunc setTemperatureAlertValue(info *system.Info, stats *system.Stats, value float64) {\n\tinfo.DashboardTemp = value\n\tstats.Temperatures = map[string]float64{\n\t\t\"Temp0\": value,\n\t}\n}\n\nfunc setLoadAvgAlertValue(info *system.Info, stats *system.Stats, value [3]float64) {\n\tinfo.LoadAvg = value\n\tstats.LoadAvg = value\n}\n\nfunc setBatteryAlertValue(info *system.Info, stats *system.Stats, value [2]uint8) {\n\tinfo.Battery = value\n\tstats.Battery = value\n}\n\nfunc TestSystemAlertsOneMin(t *testing.T) {\n\ttestOneMinuteSystemAlert(t, \"CPU\", 50, setCPUAlertValue, 51, 49)\n\ttestOneMinuteSystemAlert(t, \"Memory\", 50, setMemoryAlertValue, 51, 49)\n\ttestOneMinuteSystemAlert(t, \"Disk\", 50, setDiskAlertValue, 51, 49)\n\ttestOneMinuteSystemAlert(t, \"Bandwidth\", 50, setBandwidthAlertValue, [2]uint64{megabytesToBytes(26), megabytesToBytes(25)}, [2]uint64{megabytesToBytes(25), megabytesToBytes(24)})\n\ttestOneMinuteSystemAlert(t, \"GPU\", 50, setGPUAlertValue, 51, 49)\n\ttestOneMinuteSystemAlert(t, \"Temperature\", 70, setTemperatureAlertValue, 71, 69)\n\ttestOneMinuteSystemAlert(t, \"LoadAvg1\", 4, setLoadAvgAlertValue, [3]float64{4.1, 0, 0}, [3]float64{3.9, 0, 0})\n\ttestOneMinuteSystemAlert(t, \"LoadAvg5\", 4, setLoadAvgAlertValue, [3]float64{0, 4.1, 0}, [3]float64{0, 3.9, 0})\n\ttestOneMinuteSystemAlert(t, \"LoadAvg15\", 4, setLoadAvgAlertValue, [3]float64{0, 0, 4.1}, [3]float64{0, 0, 3.9})\n\ttestOneMinuteSystemAlert(t, \"Battery\", 20, setBatteryAlertValue, [2]uint8{19, 0}, [2]uint8{21, 0})\n}\n\nfunc TestSystemAlertsTwoMin(t *testing.T) {\n\ttestMultiMinuteSystemAlert(t, \"CPU\", 50, 2, setCPUAlertValue, 10, 51, 48)\n\ttestMultiMinuteSystemAlert(t, \"Memory\", 50, 2, setMemoryAlertValue, 10, 51, 48)\n\ttestMultiMinuteSystemAlert(t, \"Disk\", 50, 2, setDiskAlertValue, 10, 51, 48)\n\ttestMultiMinuteSystemAlert(t, \"Bandwidth\", 50, 2, setBandwidthAlertValue, [2]uint64{megabytesToBytes(10), megabytesToBytes(10)}, [2]uint64{megabytesToBytes(26), megabytesToBytes(25)}, [2]uint64{megabytesToBytes(10), megabytesToBytes(10)})\n\ttestMultiMinuteSystemAlert(t, \"GPU\", 50, 2, setGPUAlertValue, 10, 51, 48)\n\ttestMultiMinuteSystemAlert(t, \"Temperature\", 70, 2, setTemperatureAlertValue, 10, 71, 67)\n\ttestMultiMinuteSystemAlert(t, \"LoadAvg1\", 4, 2, setLoadAvgAlertValue, [3]float64{0, 0, 0}, [3]float64{4.1, 0, 0}, [3]float64{3.5, 0, 0})\n\ttestMultiMinuteSystemAlert(t, \"LoadAvg5\", 4, 2, setLoadAvgAlertValue, [3]float64{0, 2, 0}, [3]float64{0, 4.1, 0}, [3]float64{0, 3.5, 0})\n\ttestMultiMinuteSystemAlert(t, \"LoadAvg15\", 4, 2, setLoadAvgAlertValue, [3]float64{0, 0, 2}, [3]float64{0, 0, 4.1}, [3]float64{0, 0, 3.5})\n\ttestMultiMinuteSystemAlert(t, \"Battery\", 20, 2, setBatteryAlertValue, [2]uint8{21, 0}, [2]uint8{19, 0}, [2]uint8{25, 1})\n}\n"
  },
  {
    "path": "internal/alerts/alerts_test.go",
    "content": "//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\"testing/synctest\"\n\t\"time\"\n\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/henrygd/beszel/internal/alerts\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tpbTests \"github.com/pocketbase/pocketbase/tests\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// marshal to json and return an io.Reader (for use in ApiScenario.Body)\nfunc jsonReader(v any) io.Reader {\n\tdata, err := json.Marshal(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn bytes.NewReader(data)\n}\n\nfunc TestUserAlertsApi(t *testing.T) {\n\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\tdefer hub.Cleanup()\n\n\thub.StartHub()\n\n\tuser1, _ := beszelTests.CreateUser(hub, \"alertstest@example.com\", \"password\")\n\tuser1Token, _ := user1.NewAuthToken()\n\n\tuser2, _ := beszelTests.CreateUser(hub, \"alertstest2@example.com\", \"password\")\n\tuser2Token, _ := user2.NewAuthToken()\n\n\tsystem1, _ := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"system1\",\n\t\t\"users\": []string{user1.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\n\tsystem2, _ := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"system2\",\n\t\t\"users\": []string{user1.Id, user2.Id},\n\t\t\"host\":  \"127.0.0.2\",\n\t})\n\n\tuserRecords, _ := hub.CountRecords(\"users\")\n\tassert.EqualValues(t, 2, userRecords, \"all users should be created\")\n\n\tsystemRecords, _ := hub.CountRecords(\"systems\")\n\tassert.EqualValues(t, 2, systemRecords, \"all systems should be created\")\n\n\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\treturn hub.TestApp\n\t}\n\n\tscenarios := []beszelTests.ApiScenario{\n\t\t// {\n\t\t// \tName:            \"GET not implemented - returns index\",\n\t\t// \tMethod:          http.MethodGet,\n\t\t// \tURL:             \"/api/beszel/user-alerts\",\n\t\t// \tExpectedStatus:  200,\n\t\t// \tExpectedContent: []string{\"<html \", \"globalThis.BESZEL\"},\n\t\t// \tTestAppFactory:  testAppFactory,\n\t\t// },\n\t\t{\n\t\t\tName:            \"POST no auth\",\n\t\t\tMethod:          http.MethodPost,\n\t\t\tURL:             \"/api/beszel/user-alerts\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST no body\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"Bad data\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST bad data\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"Bad data\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"invalidField\": \"this should cause validation error\",\n\t\t\t\t\"threshold\":    \"not a number\",\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tName:   \"POST malformed JSON\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"Bad data\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody:            strings.NewReader(`{\"alertType\": \"cpu\", \"threshold\": 80, \"enabled\": true,}`),\n\t\t},\n\t\t{\n\t\t\tName:   \"POST valid alert data multiple systems\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":      \"CPU\",\n\t\t\t\t\"value\":     69,\n\t\t\t\t\"min\":       9,\n\t\t\t\t\"systems\":   []string{system1.Id, system2.Id},\n\t\t\t\t\"overwrite\": false,\n\t\t\t}),\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\t// check total alerts\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 2, alerts, \"should have 2 alerts\")\n\t\t\t\t// check alert has correct values\n\t\t\t\tmatchingAlerts, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"name\": \"CPU\", \"user\": user1.Id, \"system\": system1.Id, \"value\": 69, \"min\": 9})\n\t\t\t\tassert.EqualValues(t, 1, matchingAlerts, \"should have 1 alert\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"POST valid alert data single system\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"Memory\",\n\t\t\t\t\"systems\": []string{system1.Id},\n\t\t\t\t\"value\":   90,\n\t\t\t\t\"min\":     10,\n\t\t\t}),\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\tuser1Alerts, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"user\": user1.Id})\n\t\t\t\tassert.EqualValues(t, 3, user1Alerts, \"should have 3 alerts\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"Overwrite: false, should not overwrite existing alert\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":      \"CPU\",\n\t\t\t\t\"value\":     45,\n\t\t\t\t\"min\":       5,\n\t\t\t\t\"systems\":   []string{system1.Id},\n\t\t\t\t\"overwrite\": false,\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\"system\": system1.Id,\n\t\t\t\t\t\"user\":   user1.Id,\n\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\"min\":    10,\n\t\t\t\t})\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 1, alerts, \"should have 1 alert\")\n\t\t\t\talert, _ := app.FindFirstRecordByFilter(\"alerts\", \"name = 'CPU' && user = {:user}\", dbx.Params{\"user\": user1.Id})\n\t\t\t\tassert.EqualValues(t, 80, alert.Get(\"value\"), \"should have 80 as value\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"Overwrite: true, should overwrite existing alert\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user2Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":      \"CPU\",\n\t\t\t\t\"value\":     45,\n\t\t\t\t\"min\":       5,\n\t\t\t\t\"systems\":   []string{system2.Id},\n\t\t\t\t\"overwrite\": true,\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\"system\": system2.Id,\n\t\t\t\t\t\"user\":   user2.Id,\n\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\"min\":    10,\n\t\t\t\t})\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 1, alerts, \"should have 1 alert\")\n\t\t\t\talert, _ := app.FindFirstRecordByFilter(\"alerts\", \"name = 'CPU' && user = {:user}\", dbx.Params{\"user\": user2.Id})\n\t\t\t\tassert.EqualValues(t, 45, alert.Get(\"value\"), \"should have 45 as value\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:            \"DELETE no auth\",\n\t\t\tMethod:          http.MethodDelete,\n\t\t\tURL:             \"/api/beszel/user-alerts\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"systems\": []string{system1.Id},\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\"system\": system1.Id,\n\t\t\t\t\t\"user\":   user1.Id,\n\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\"min\":    10,\n\t\t\t\t})\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 1, alerts, \"should have 1 alert\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"DELETE alert\",\n\t\t\tMethod: http.MethodDelete,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"count\\\":1\", \"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"systems\": []string{system1.Id},\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\"system\": system1.Id,\n\t\t\t\t\t\"user\":   user1.Id,\n\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\"min\":    10,\n\t\t\t\t})\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.Zero(t, alerts, \"should have 0 alerts\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"DELETE alert multiple systems\",\n\t\t\tMethod: http.MethodDelete,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user1Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"count\\\":2\", \"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"Memory\",\n\t\t\t\t\"systems\": []string{system1.Id, system2.Id},\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tfor _, systemId := range []string{system1.Id, system2.Id} {\n\t\t\t\t\t_, err := beszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\t\"name\":   \"Memory\",\n\t\t\t\t\t\t\"system\": systemId,\n\t\t\t\t\t\t\"user\":   user1.Id,\n\t\t\t\t\t\t\"value\":  90,\n\t\t\t\t\t\t\"min\":    10,\n\t\t\t\t\t})\n\t\t\t\t\tassert.NoError(t, err, \"should create alert\")\n\t\t\t\t}\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 2, alerts, \"should have 2 alerts\")\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.Zero(t, alerts, \"should have 0 alerts\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"User 2 should not be able to delete alert of user 1\",\n\t\t\tMethod: http.MethodDelete,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": user2Token,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"count\\\":1\", \"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"systems\": []string{system2.Id},\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.ClearCollection(t, app, \"alerts\")\n\t\t\t\tfor _, user := range []string{user1.Id, user2.Id} {\n\t\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\t\"system\": system2.Id,\n\t\t\t\t\t\t\"user\":   user,\n\t\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\t\"min\":    10,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\talerts, _ := app.CountRecords(\"alerts\")\n\t\t\t\tassert.EqualValues(t, 2, alerts, \"should have 2 alerts\")\n\t\t\t\tuser1AlertCount, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"user\": user1.Id})\n\t\t\t\tassert.EqualValues(t, 1, user1AlertCount, \"should have 1 alert\")\n\t\t\t\tuser2AlertCount, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"user\": user2.Id})\n\t\t\t\tassert.EqualValues(t, 1, user2AlertCount, \"should have 1 alert\")\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\tuser1AlertCount, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"user\": user1.Id})\n\t\t\t\tassert.EqualValues(t, 1, user1AlertCount, \"should have 1 alert\")\n\t\t\t\tuser2AlertCount, _ := app.CountRecords(\"alerts\", dbx.HashExp{\"user\": user2.Id})\n\t\t\t\tassert.Zero(t, user2AlertCount, \"should have 0 alerts\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, scenario := range scenarios {\n\t\tscenario.Test(t)\n\t}\n}\n\nfunc TestAlertsHistory(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\thub, user := beszelTests.GetHubWithUser(t)\n\t\tdefer hub.Cleanup()\n\n\t\t// Create systems and alerts\n\t\tsystems, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\t\tassert.NoError(t, err)\n\t\tsystem := systems[0]\n\n\t\talert, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system.Id,\n\t\t\t\"user\":   user.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Initially, no alert history records should exist\n\t\tinitialHistoryCount, err := hub.CountRecords(\"alerts_history\", nil)\n\t\tassert.NoError(t, err)\n\t\tassert.Zero(t, initialHistoryCount, \"Should have 0 alert history records initially\")\n\n\t\t// Set system to up initially\n\t\tsystem.Set(\"status\", \"up\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t// Set system to down to trigger alert\n\t\tsystem.Set(\"status\", \"down\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\n\t\t// Wait for alert to trigger (after the downtime delay)\n\t\t// With 1 minute delay, we need to wait at least 1 minute + some buffer\n\t\ttime.Sleep(time.Second * 75)\n\n\t\t// Check that alert is triggered\n\t\ttriggeredCount, err := hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true, \"id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, triggeredCount, \"Alert should be triggered\")\n\n\t\t// Check that alert history record was created\n\t\thistoryCount, err := hub.CountRecords(\"alerts_history\", dbx.HashExp{\"alert_id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, historyCount, \"Should have 1 alert history record for triggered alert\")\n\n\t\t// Get the alert history record and verify it's not resolved immediately\n\t\thistoryRecord, err := hub.FindFirstRecordByFilter(\"alerts_history\", \"alert_id={:alert_id}\", dbx.Params{\"alert_id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, historyRecord, \"Alert history record should exist\")\n\t\tassert.Equal(t, alert.Id, historyRecord.GetString(\"alert_id\"), \"Alert history should reference correct alert\")\n\t\tassert.Equal(t, system.Id, historyRecord.GetString(\"system\"), \"Alert history should reference correct system\")\n\t\tassert.Equal(t, \"Status\", historyRecord.GetString(\"name\"), \"Alert history should have correct name\")\n\n\t\t// The alert history might be resolved immediately in some cases, so let's check the alert's triggered status\n\t\talertRecord, err := hub.FindFirstRecordByFilter(\"alerts\", \"id={:id}\", dbx.Params{\"id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, alertRecord.GetBool(\"triggered\"), \"Alert should still be triggered when checking history\")\n\n\t\t// Now resolve the alert by setting system back to up\n\t\tsystem.Set(\"status\", \"up\")\n\t\terr = hub.SaveNoValidate(system)\n\t\tassert.NoError(t, err)\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t// Check that alert is no longer triggered\n\t\ttriggeredCount, err = hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true, \"id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.Zero(t, triggeredCount, \"Alert should not be triggered after system is back up\")\n\n\t\t// Check that alert history record is now resolved\n\t\thistoryRecord, err = hub.FindFirstRecordByFilter(\"alerts_history\", \"alert_id={:alert_id}\", dbx.Params{\"alert_id\": alert.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, historyRecord, \"Alert history record should still exist\")\n\t\tassert.NotNil(t, historyRecord.Get(\"resolved\"), \"Alert history should be resolved\")\n\n\t\t// Test deleting a triggered alert resolves its history\n\t\t// Create another system and alert\n\t\tsystems2, err := beszelTests.CreateSystems(hub, 1, user.Id, \"up\")\n\t\tassert.NoError(t, err)\n\t\tsystem2 := systems2[0]\n\t\tsystem2.Set(\"name\", \"test-system-2\") // Rename for clarity\n\t\terr = hub.SaveNoValidate(system2)\n\t\tassert.NoError(t, err)\n\n\t\talert2, err := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\t\"name\":   \"Status\",\n\t\t\t\"system\": system2.Id,\n\t\t\t\"user\":   user.Id,\n\t\t\t\"min\":    1,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// Set system2 to down to trigger alert\n\t\tsystem2.Set(\"status\", \"down\")\n\t\terr = hub.SaveNoValidate(system2)\n\t\tassert.NoError(t, err)\n\n\t\t// Wait for alert to trigger\n\t\ttime.Sleep(time.Second * 75)\n\n\t\t// Verify alert is triggered and history record exists\n\t\ttriggeredCount, err = hub.CountRecords(\"alerts\", dbx.HashExp{\"triggered\": true, \"id\": alert2.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, triggeredCount, \"Second alert should be triggered\")\n\n\t\thistoryCount, err = hub.CountRecords(\"alerts_history\", dbx.HashExp{\"alert_id\": alert2.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 1, historyCount, \"Should have 1 alert history record for second alert\")\n\n\t\t// Delete the triggered alert\n\t\terr = hub.Delete(alert2)\n\t\tassert.NoError(t, err)\n\n\t\t// Check that alert history record is resolved after deletion\n\t\thistoryRecord2, err := hub.FindFirstRecordByFilter(\"alerts_history\", \"alert_id={:alert_id}\", dbx.Params{\"alert_id\": alert2.Id})\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, historyRecord2, \"Alert history record should still exist after alert deletion\")\n\t\tassert.NotNil(t, historyRecord2.Get(\"resolved\"), \"Alert history should be resolved after alert deletion\")\n\n\t\t// Verify total history count is correct (2 records total)\n\t\ttotalHistoryCount, err := hub.CountRecords(\"alerts_history\", nil)\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, 2, totalHistoryCount, \"Should have 2 total alert history records\")\n\t})\n}\n\nfunc TestSetAlertTriggered(t *testing.T) {\n\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\tdefer hub.Cleanup()\n\n\thub.StartHub()\n\n\tuser, _ := beszelTests.CreateUser(hub, \"test@example.com\", \"password\")\n\tsystem, _ := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\n\talertRecord, _ := beszelTests.CreateRecord(hub, \"alerts\", map[string]any{\n\t\t\"name\":      \"CPU\",\n\t\t\"system\":    system.Id,\n\t\t\"user\":      user.Id,\n\t\t\"value\":     80,\n\t\t\"triggered\": false,\n\t})\n\n\tam := alerts.NewAlertManager(hub)\n\n\tvar alert alerts.CachedAlertData\n\talert.PopulateFromRecord(alertRecord)\n\n\t// Test triggering the alert\n\terr := am.SetAlertTriggered(alert, true)\n\tassert.NoError(t, err)\n\n\tupdatedRecord, err := hub.FindRecordById(\"alerts\", alert.Id)\n\tassert.NoError(t, err)\n\tassert.True(t, updatedRecord.GetBool(\"triggered\"))\n\n\t// Test un-triggering the alert\n\terr = am.SetAlertTriggered(alert, false)\n\tassert.NoError(t, err)\n\n\tupdatedRecord, err = hub.FindRecordById(\"alerts\", alert.Id)\n\tassert.NoError(t, err)\n\tassert.False(t, updatedRecord.GetBool(\"triggered\"))\n}\n"
  },
  {
    "path": "internal/alerts/alerts_test_helpers.go",
    "content": "//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 NewTestAlertManagerWithoutWorker(app hubLike) *AlertManager {\n\treturn &AlertManager{\n\t\thub:         app,\n\t\talertsCache: NewAlertsCache(app),\n\t}\n}\n\n// GetSystemAlertsCache returns the internal system alerts cache.\nfunc (am *AlertManager) GetSystemAlertsCache() *AlertsCache {\n\treturn am.alertsCache\n}\n\nfunc (am *AlertManager) GetAlertManager() *AlertManager {\n\treturn am\n}\n\nfunc (am *AlertManager) GetPendingAlerts() *sync.Map {\n\treturn &am.pendingAlerts\n}\n\nfunc (am *AlertManager) GetPendingAlertsCount() int {\n\tcount := 0\n\tam.pendingAlerts.Range(func(key, value any) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\treturn count\n}\n\n// ProcessPendingAlerts manually processes all expired alerts (for testing)\nfunc (am *AlertManager) ProcessPendingAlerts() ([]CachedAlertData, error) {\n\tnow := time.Now()\n\tvar lastErr error\n\tvar processedAlerts []CachedAlertData\n\tam.pendingAlerts.Range(func(key, value any) bool {\n\t\tinfo := value.(*alertInfo)\n\t\tif now.After(info.expireTime) {\n\t\t\tif info.timer != nil {\n\t\t\t\tinfo.timer.Stop()\n\t\t\t}\n\t\t\tam.processPendingAlert(key.(string))\n\t\t\tprocessedAlerts = append(processedAlerts, info.alertData)\n\t\t}\n\t\treturn true\n\t})\n\treturn processedAlerts, lastErr\n}\n\n// ForceExpirePendingAlerts sets all pending alerts to expire immediately (for testing)\nfunc (am *AlertManager) ForceExpirePendingAlerts() {\n\tnow := time.Now()\n\tam.pendingAlerts.Range(func(key, value any) bool {\n\t\tinfo := value.(*alertInfo)\n\t\tinfo.expireTime = now.Add(-time.Second) // Set to 1 second ago\n\t\treturn true\n\t})\n}\n\nfunc (am *AlertManager) ResetPendingAlertTimer(alertID string, delay time.Duration) bool {\n\tvalue, loaded := am.pendingAlerts.Load(alertID)\n\tif !loaded {\n\t\treturn false\n\t}\n\n\tinfo := value.(*alertInfo)\n\tif info.timer != nil {\n\t\tinfo.timer.Stop()\n\t}\n\tinfo.expireTime = time.Now().Add(delay)\n\tinfo.timer = time.AfterFunc(delay, func() {\n\t\tam.processPendingAlert(alertID)\n\t})\n\treturn true\n}\n\nfunc ResolveStatusAlerts(app core.App) error {\n\treturn resolveStatusAlerts(app)\n}\n\nfunc (am *AlertManager) RestorePendingStatusAlerts() error {\n\treturn am.restorePendingStatusAlerts()\n}\n\nfunc (am *AlertManager) SetAlertTriggered(alert CachedAlertData, triggered bool) error {\n\treturn am.setAlertTriggered(alert, triggered)\n}\n"
  },
  {
    "path": "internal/cmd/agent/agent.go",
    "content": "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\"github.com/henrygd/beszel/agent/health\"\n\t\"github.com/henrygd/beszel/agent/utils\"\n\t\"github.com/spf13/pflag\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// cli options\ntype cmdOptions struct {\n\tkey    string // key is the public key(s) for SSH authentication.\n\tlisten string // listen is the address or port to listen on.\n\thubURL string // hubURL is the URL of the Beszel hub.\n\ttoken  string // token is the token to use for authentication.\n}\n\n// parse parses the command line flags and populates the config struct.\n// It returns true if a subcommand was handled and the program should exit.\nfunc (opts *cmdOptions) parse() bool {\n\tsubcommand := \"\"\n\tif len(os.Args) > 1 {\n\t\tsubcommand = os.Args[1]\n\t}\n\n\t// Subcommands that don't require any pflag parsing\n\tswitch subcommand {\n\tcase \"health\":\n\t\terr := health.Check()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tfmt.Print(\"ok\")\n\t\treturn true\n\tcase \"fingerprint\":\n\t\thandleFingerprint()\n\t\treturn true\n\t}\n\n\t// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true\n\tpflag.StringVarP(&opts.key, \"key\", \"k\", \"\", \"Public key(s) for SSH authentication\")\n\tpflag.StringVarP(&opts.listen, \"listen\", \"l\", \"\", \"Address or port to listen on\")\n\tpflag.StringVarP(&opts.hubURL, \"url\", \"u\", \"\", \"URL of the Beszel hub\")\n\tpflag.StringVarP(&opts.token, \"token\", \"t\", \"\", \"Token to use for authentication\")\n\tchinaMirrors := pflag.BoolP(\"china-mirrors\", \"c\", false, \"Use mirror for update (gh.beszel.dev) instead of GitHub\")\n\tversion := pflag.BoolP(\"version\", \"v\", false, \"Show version information\")\n\thelp := pflag.BoolP(\"help\", \"h\", false, \"Show this help message\")\n\n\t// Convert old single-dash long flags to double-dash for backward compatibility\n\tflagsToConvert := []string{\"key\", \"listen\", \"url\", \"token\"}\n\tfor i, arg := range os.Args {\n\t\tfor _, flag := range flagsToConvert {\n\t\t\tsingleDash := \"-\" + flag\n\t\t\tdoubleDash := \"--\" + flag\n\t\t\tif arg == singleDash {\n\t\t\t\tos.Args[i] = doubleDash\n\t\t\t\tbreak\n\t\t\t} else if strings.HasPrefix(arg, singleDash+\"=\") {\n\t\t\t\tos.Args[i] = doubleDash + arg[len(singleDash):]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tpflag.Usage = func() {\n\t\tbuilder := strings.Builder{}\n\t\tbuilder.WriteString(\"Usage: \")\n\t\tbuilder.WriteString(os.Args[0])\n\t\tbuilder.WriteString(\" [command] [flags]\\n\")\n\t\tbuilder.WriteString(\"\\nCommands:\\n\")\n\t\tbuilder.WriteString(\"  fingerprint  View or reset the agent fingerprint\\n\")\n\t\tbuilder.WriteString(\"  health       Check if the agent is running\\n\")\n\t\tbuilder.WriteString(\"  update       Update to the latest version\\n\")\n\t\tbuilder.WriteString(\"\\nFlags:\\n\")\n\t\tfmt.Print(builder.String())\n\t\tpflag.PrintDefaults()\n\t}\n\n\t// Parse all arguments with pflag\n\tpflag.Parse()\n\n\t// Must run after pflag.Parse()\n\tswitch {\n\tcase *version:\n\t\tfmt.Println(beszel.AppName+\"-agent\", beszel.Version)\n\t\treturn true\n\tcase *help || subcommand == \"help\":\n\t\tpflag.Usage()\n\t\treturn true\n\tcase subcommand == \"update\":\n\t\tagent.Update(*chinaMirrors)\n\t\treturn true\n\t}\n\n\t// Set environment variables from CLI flags (if provided)\n\tif opts.hubURL != \"\" {\n\t\tos.Setenv(\"HUB_URL\", opts.hubURL)\n\t}\n\tif opts.token != \"\" {\n\t\tos.Setenv(\"TOKEN\", opts.token)\n\t}\n\treturn false\n}\n\n// loadPublicKeys loads the public keys from the command line flag, environment variable, or key file.\nfunc (opts *cmdOptions) loadPublicKeys() ([]ssh.PublicKey, error) {\n\t// Try command line flag first\n\tif opts.key != \"\" {\n\t\treturn agent.ParseKeys(opts.key)\n\t}\n\n\t// Try environment variable\n\tif key, ok := utils.GetEnv(\"KEY\"); ok && key != \"\" {\n\t\treturn agent.ParseKeys(key)\n\t}\n\n\t// Try key file\n\tkeyFile, ok := utils.GetEnv(\"KEY_FILE\")\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no key provided: must set -key flag, KEY env var, or KEY_FILE env var. Use 'beszel-agent help' for usage\")\n\t}\n\n\tpubKey, err := os.ReadFile(keyFile)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read key file: %w\", err)\n\t}\n\treturn agent.ParseKeys(string(pubKey))\n}\n\nfunc (opts *cmdOptions) getAddress() string {\n\treturn agent.GetAddress(opts.listen)\n}\n\n// handleFingerprint handles the \"fingerprint\" command with subcommands \"view\" and \"reset\".\nfunc handleFingerprint() {\n\tsubCmd := \"\"\n\tif len(os.Args) > 2 {\n\t\tsubCmd = os.Args[2]\n\t}\n\n\tswitch subCmd {\n\tcase \"\", \"view\":\n\t\tdataDir, _ := agent.GetDataDir()\n\t\tfp := agent.GetFingerprint(dataDir, \"\", \"\")\n\t\tfmt.Println(fp)\n\tcase \"help\", \"-h\", \"--help\":\n\t\tfmt.Print(fingerprintUsage())\n\tcase \"reset\":\n\t\tdataDir, err := agent.GetDataDir()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tif err := agent.DeleteFingerprint(dataDir); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tfmt.Println(\"Fingerprint reset. A new one will be generated on next start.\")\n\tdefault:\n\t\tlog.Fatalf(\"Unknown command: %q\\n\\n%s\", subCmd, fingerprintUsage())\n\t}\n}\n\nfunc fingerprintUsage() string {\n\treturn fmt.Sprintf(\"Usage: %s fingerprint [view|reset]\\n\\nCommands:\\n  view   Print fingerprint (default)\\n  reset  Reset saved fingerprint\\n\", os.Args[0])\n}\n\nfunc main() {\n\tvar opts cmdOptions\n\tsubcommandHandled := opts.parse()\n\n\tif subcommandHandled {\n\t\treturn\n\t}\n\n\tvar serverConfig agent.ServerOptions\n\tvar err error\n\tserverConfig.Keys, err = opts.loadPublicKeys()\n\tif err != nil {\n\t\tlog.Fatal(\"Failed to load public keys:\", err)\n\t}\n\n\taddr := opts.getAddress()\n\tserverConfig.Addr = addr\n\tserverConfig.Network = agent.GetNetwork(addr)\n\n\ta, err := agent.NewAgent()\n\tif err != nil {\n\t\tlog.Fatal(\"Failed to create agent: \", err)\n\t}\n\n\tif err := a.Start(serverConfig); err != nil {\n\t\tlog.Fatal(\"Failed to start server: \", err)\n\t}\n}\n"
  },
  {
    "path": "internal/cmd/agent/agent_test.go",
    "content": "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.com/spf13/pflag\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestGetAddress(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\topts     cmdOptions\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"default port when no config\",\n\t\t\topts:     cmdOptions{},\n\t\t\texpected: \":45876\",\n\t\t},\n\t\t{\n\t\t\tname: \"use address from flag\",\n\t\t\topts: cmdOptions{\n\t\t\t\tlisten: \"8080\",\n\t\t\t},\n\t\t\texpected: \":8080\",\n\t\t},\n\t\t{\n\t\t\tname: \"use unix socket from flag\",\n\t\t\topts: cmdOptions{\n\t\t\t\tlisten: \"/tmp/beszel.sock\",\n\t\t\t},\n\t\t\texpected: \"/tmp/beszel.sock\",\n\t\t},\n\t\t{\n\t\t\tname: \"use LISTEN env var\",\n\t\t\topts: cmdOptions{},\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"LISTEN\": \"1.2.3.4:9090\",\n\t\t\t},\n\t\t\texpected: \"1.2.3.4:9090\",\n\t\t},\n\t\t{\n\t\t\tname: \"use legacy PORT env var\",\n\t\t\topts: cmdOptions{},\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"PORT\": \"7070\",\n\t\t\t},\n\t\t\texpected: \":7070\",\n\t\t},\n\t\t{\n\t\t\tname: \"use unix socket from env var\",\n\t\t\topts: cmdOptions{\n\t\t\t\tlisten: \"\",\n\t\t\t},\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"LISTEN\": \"/tmp/beszel.sock\",\n\t\t\t},\n\t\t\texpected: \"/tmp/beszel.sock\",\n\t\t},\n\t\t{\n\t\t\tname: \"flag takes precedence over env vars\",\n\t\t\topts: cmdOptions{\n\t\t\t\tlisten: \":8080\",\n\t\t\t},\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"LISTEN\": \":9090\",\n\t\t\t\t\"PORT\":   \"7070\",\n\t\t\t},\n\t\t\texpected: \":8080\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Setup environment\n\t\t\tfor k, v := range tt.envVars {\n\t\t\t\tt.Setenv(k, v)\n\t\t\t}\n\n\t\t\taddr := tt.opts.getAddress()\n\t\t\tassert.Equal(t, tt.expected, addr)\n\t\t})\n\t}\n}\n\nfunc TestLoadPublicKeys(t *testing.T) {\n\t// Generate a test key\n\t_, priv, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tsigner, err := ssh.NewSignerFromKey(priv)\n\trequire.NoError(t, err)\n\tpubKey := ssh.MarshalAuthorizedKey(signer.PublicKey())\n\n\ttests := []struct {\n\t\tname        string\n\t\topts        cmdOptions\n\t\tenvVars     map[string]string\n\t\tsetupFiles  map[string][]byte\n\t\twantErr     bool\n\t\terrContains string\n\t}{\n\t\t{\n\t\t\tname: \"load key from flag\",\n\t\t\topts: cmdOptions{\n\t\t\t\tkey: string(pubKey),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"load key from env var\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"KEY\": string(pubKey),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"load key from file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"KEY_FILE\": \"testkey.pub\",\n\t\t\t},\n\t\t\tsetupFiles: map[string][]byte{\n\t\t\t\t\"testkey.pub\": pubKey,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"error when no key provided\",\n\t\t\twantErr:     true,\n\t\t\terrContains: \"no key provided\",\n\t\t},\n\t\t{\n\t\t\tname: \"error on invalid key file\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"KEY_FILE\": \"nonexistent.pub\",\n\t\t\t},\n\t\t\twantErr:     true,\n\t\t\terrContains: \"failed to read key file\",\n\t\t},\n\t\t{\n\t\t\tname: \"error on invalid key data\",\n\t\t\topts: cmdOptions{\n\t\t\t\tkey: \"invalid-key-data\",\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a temporary directory for test files\n\t\t\tif len(tt.setupFiles) > 0 {\n\t\t\t\ttmpDir := t.TempDir()\n\t\t\t\tfor name, content := range tt.setupFiles {\n\t\t\t\t\tpath := filepath.Join(tmpDir, name)\n\t\t\t\t\terr := os.WriteFile(path, content, 0600)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tif tt.envVars != nil {\n\t\t\t\t\t\ttt.envVars[\"KEY_FILE\"] = path\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set up environment\n\t\t\tfor k, v := range tt.envVars {\n\t\t\t\tt.Setenv(k, v)\n\t\t\t}\n\n\t\t\tkeys, err := tt.opts.loadPublicKeys()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tif tt.errContains != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.errContains)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, keys, 1)\n\t\t\tassert.Equal(t, signer.PublicKey().Type(), keys[0].Type())\n\t\t})\n\t}\n}\n\nfunc TestGetNetwork(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\topts     cmdOptions\n\t\tenvVars  map[string]string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"NETWORK env var\",\n\t\t\tenvVars: map[string]string{\n\t\t\t\t\"NETWORK\": \"tcp4\",\n\t\t\t},\n\t\t\texpected: \"tcp4\",\n\t\t},\n\t\t{\n\t\t\tname:     \"only port\",\n\t\t\topts:     cmdOptions{listen: \"8080\"},\n\t\t\texpected: \"tcp\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ipv4 address\",\n\t\t\topts:     cmdOptions{listen: \"1.2.3.4:8080\"},\n\t\t\texpected: \"tcp\",\n\t\t},\n\t\t{\n\t\t\tname:     \"ipv6 address\",\n\t\t\topts:     cmdOptions{listen: \"[2001:db8::1]:8080\"},\n\t\t\texpected: \"tcp\",\n\t\t},\n\t\t{\n\t\t\tname:     \"unix network\",\n\t\t\topts:     cmdOptions{listen: \"/tmp/beszel.sock\"},\n\t\t\texpected: \"unix\",\n\t\t},\n\t\t{\n\t\t\tname:     \"env var network\",\n\t\t\topts:     cmdOptions{listen: \":8080\"},\n\t\t\tenvVars:  map[string]string{\"NETWORK\": \"tcp4\"},\n\t\t\texpected: \"tcp4\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Setup environment\n\t\t\tfor k, v := range tt.envVars {\n\t\t\t\tt.Setenv(k, v)\n\t\t\t}\n\t\t\tnetwork := agent.GetNetwork(tt.opts.listen)\n\t\t\tassert.Equal(t, tt.expected, network)\n\t\t})\n\t}\n}\n\nfunc TestParseFlags(t *testing.T) {\n\t// Save original command line arguments and restore after test\n\toldArgs := os.Args\n\tdefer func() {\n\t\tos.Args = oldArgs\n\t\tpflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)\n\t}()\n\n\ttests := []struct {\n\t\tname     string\n\t\targs     []string\n\t\texpected cmdOptions\n\t}{\n\t\t{\n\t\t\tname: \"no flags\",\n\t\t\targs: []string{\"cmd\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"\",\n\t\t\t\tlisten: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"key flag only\",\n\t\t\targs: []string{\"cmd\", \"-key\", \"testkey\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"testkey\",\n\t\t\t\tlisten: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"key flag double dash\",\n\t\t\targs: []string{\"cmd\", \"--key\", \"testkey\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"testkey\",\n\t\t\t\tlisten: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"key flag short\",\n\t\t\targs: []string{\"cmd\", \"-k\", \"testkey\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"testkey\",\n\t\t\t\tlisten: \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"addr flag only\",\n\t\t\targs: []string{\"cmd\", \"-listen\", \":8080\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"\",\n\t\t\t\tlisten: \":8080\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"addr flag double dash\",\n\t\t\targs: []string{\"cmd\", \"--listen\", \":8080\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"\",\n\t\t\t\tlisten: \":8080\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"addr flag short\",\n\t\t\targs: []string{\"cmd\", \"-l\", \":8080\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"\",\n\t\t\t\tlisten: \":8080\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"both flags\",\n\t\t\targs: []string{\"cmd\", \"-key\", \"testkey\", \"-listen\", \":8080\"},\n\t\t\texpected: cmdOptions{\n\t\t\t\tkey:    \"testkey\",\n\t\t\t\tlisten: \":8080\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Reset flags for each test\n\t\t\tpflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)\n\t\t\tos.Args = tt.args\n\n\t\t\tvar opts cmdOptions\n\t\t\topts.parse()\n\t\t\tpflag.Parse()\n\n\t\t\tassert.Equal(t, tt.expected, opts)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cmd/hub/hub.go",
    "content": "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/internal/hub\"\n\t_ \"github.com/henrygd/beszel/internal/migrations\"\n\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/plugins/migratecmd\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc main() {\n\t// handle health check first to prevent unneeded execution\n\tif len(os.Args) > 3 && os.Args[1] == \"health\" {\n\t\turl := os.Args[3]\n\t\tif err := checkHealth(url); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tfmt.Print(\"ok\")\n\t\treturn\n\t}\n\n\tbaseApp := getBaseApp()\n\th := hub.NewHub(baseApp)\n\tif err := h.StartHub(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\n// getBaseApp creates a new PocketBase app with the default config\nfunc getBaseApp() *pocketbase.PocketBase {\n\tisDev := os.Getenv(\"ENV\") == \"dev\"\n\n\tbaseApp := pocketbase.NewWithConfig(pocketbase.Config{\n\t\tDefaultDataDir: beszel.AppName + \"_data\",\n\t\tDefaultDev:     isDev,\n\t})\n\tbaseApp.RootCmd.Version = beszel.Version\n\tbaseApp.RootCmd.Use = beszel.AppName\n\tbaseApp.RootCmd.Short = \"\"\n\t// add update command\n\tupdateCmd := &cobra.Command{\n\t\tUse:   \"update\",\n\t\tShort: \"Update \" + beszel.AppName + \" to the latest version\",\n\t\tRun:   hub.Update,\n\t}\n\tupdateCmd.Flags().Bool(\"china-mirrors\", false, \"Use mirror (gh.beszel.dev) instead of GitHub\")\n\tbaseApp.RootCmd.AddCommand(updateCmd)\n\t// add health command\n\tbaseApp.RootCmd.AddCommand(newHealthCmd())\n\n\t// enable auto creation of migration files when making collection changes in the Admin UI\n\tmigratecmd.MustRegister(baseApp, baseApp.RootCmd, migratecmd.Config{\n\t\tAutomigrate: isDev,\n\t\tDir:         \"../../migrations\",\n\t})\n\n\treturn baseApp\n}\n\nfunc newHealthCmd() *cobra.Command {\n\tvar baseURL string\n\n\thealthCmd := &cobra.Command{\n\t\tUse:   \"health\",\n\t\tShort: \"Check health of running hub\",\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif err := checkHealth(baseURL); err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t}\n\t\t\tos.Exit(0)\n\t\t},\n\t}\n\thealthCmd.Flags().StringVar(&baseURL, \"url\", \"\", \"base URL\")\n\thealthCmd.MarkFlagRequired(\"url\")\n\treturn healthCmd\n}\n\n// checkHealth checks the health of the hub.\nfunc checkHealth(baseURL string) error {\n\tclient := &http.Client{\n\t\tTimeout: time.Second * 3,\n\t}\n\thealthURL := baseURL + \"/api/health\"\n\tresp, err := client.Get(healthURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\treturn fmt.Errorf(\"%s returned status %d\", healthURL, resp.StatusCode)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/common/common-ssh.go",
    "content": "package common\n\nvar (\n\t// Allowed ssh key exchanges\n\tDefaultKeyExchanges = []string{\"curve25519-sha256\"}\n\t// Allowed ssh macs\n\tDefaultMACs = []string{\"hmac-sha2-256-etm@openssh.com\"}\n\t// Allowed ssh ciphers\n\tDefaultCiphers = []string{\"chacha20-poly1305@openssh.com\"}\n)\n"
  },
  {
    "path": "internal/common/common-ws.go",
    "content": "package common\n\nimport (\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\ntype WebSocketAction = uint8\n\nconst (\n\t// Request system data from agent\n\tGetData WebSocketAction = iota\n\t// Check the fingerprint of the agent\n\tCheckFingerprint\n\t// Request container logs from agent\n\tGetContainerLogs\n\t// Request container info from agent\n\tGetContainerInfo\n\t// Request SMART data from agent\n\tGetSmartData\n\t// Request detailed systemd service info from agent\n\tGetSystemdInfo\n\t// Add new actions here...\n)\n\n// HubRequest defines the structure for requests sent from hub to agent.\ntype HubRequest[T any] struct {\n\tAction WebSocketAction `cbor:\"0,keyasint\"`\n\tData   T               `cbor:\"1,keyasint,omitempty,omitzero\"`\n\tId     *uint32         `cbor:\"2,keyasint,omitempty\"`\n}\n\n// AgentResponse defines the structure for responses sent from agent to hub.\ntype AgentResponse struct {\n\tId          *uint32                    `cbor:\"0,keyasint,omitempty\"`\n\tSystemData  *system.CombinedData       `cbor:\"1,keyasint,omitempty,omitzero\"` // Legacy (<= 0.17)\n\tFingerprint *FingerprintResponse       `cbor:\"2,keyasint,omitempty,omitzero\"` // Legacy (<= 0.17)\n\tError       string                     `cbor:\"3,keyasint,omitempty,omitzero\"`\n\tString      *string                    `cbor:\"4,keyasint,omitempty,omitzero\"` // Legacy (<= 0.17)\n\tSmartData   map[string]smart.SmartData `cbor:\"5,keyasint,omitempty,omitzero\"` // Legacy (<= 0.17)\n\tServiceInfo systemd.ServiceDetails     `cbor:\"6,keyasint,omitempty,omitzero\"` // Legacy (<= 0.17)\n\t// Data is the generic response payload for new endpoints (0.18+)\n\tData cbor.RawMessage `cbor:\"7,keyasint,omitempty,omitzero\"`\n}\n\ntype FingerprintRequest struct {\n\tSignature   []byte `cbor:\"0,keyasint\"`\n\tNeedSysInfo bool   `cbor:\"1,keyasint\"` // For universal token system creation\n}\n\ntype FingerprintResponse struct {\n\tFingerprint string `cbor:\"0,keyasint\"`\n\t// Optional system info for universal token system creation\n\tHostname string `cbor:\"1,keyasint,omitzero\"`\n\tPort     string `cbor:\"2,keyasint,omitzero\"`\n\tName     string `cbor:\"3,keyasint,omitzero\"`\n}\n\ntype DataRequestOptions struct {\n\tCacheTimeMs    uint16 `cbor:\"0,keyasint\"`\n\tIncludeDetails bool   `cbor:\"1,keyasint\"`\n}\n\ntype ContainerLogsRequest struct {\n\tContainerID string `cbor:\"0,keyasint\"`\n}\n\ntype ContainerInfoRequest struct {\n\tContainerID string `cbor:\"0,keyasint\"`\n}\n\ntype SystemdInfoRequest struct {\n\tServiceName string `cbor:\"0,keyasint\"`\n}\n"
  },
  {
    "path": "internal/dockerfile_agent",
    "content": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n# Copy source files\nCOPY . ./\n\n# Build\nARG TARGETOS TARGETARCH\nRUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags \"-w -s\" -o /agent ./internal/cmd/agent\n\nRUN rm -rf /tmp/*\n\n# --------------------------\n# Final image: default scratch-based agent\n# --------------------------\nFROM scratch\nCOPY --from=builder /agent /agent\n\n# this is so we don't need to create the /tmp directory in the scratch container\nCOPY --from=builder /tmp /tmp\n\n# AMD GPU name lookup (used by agent on Linux when /usr/share/libdrm/amdgpu.ids is read)\nCOPY --from=builder /app/agent/test-data/amdgpu.ids /usr/share/libdrm/amdgpu.ids\n\n# Ensure data persistence across container recreations\nVOLUME [\"/var/lib/beszel-agent\"]\n\nENTRYPOINT [\"/agent\"]"
  },
  {
    "path": "internal/dockerfile_agent_alpine",
    "content": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n# Copy source files\nCOPY . ./\n\n# Build\nARG TARGETOS TARGETARCH\nRUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags \"-w -s\" -o /agent ./internal/cmd/agent\n\nRUN rm -rf /tmp/*\n\n# --------------------------\n# Final image: default scratch-based agent\n# --------------------------\nFROM alpine:3.23\nCOPY --from=builder /agent /agent\n\n# AMD GPU name lookup (used by agent on Linux when /usr/share/libdrm/amdgpu.ids is read)\nCOPY --from=builder /app/agent/test-data/amdgpu.ids /usr/share/libdrm/amdgpu.ids\n\nRUN apk add --no-cache smartmontools\n\n# Ensure data persistence across container recreations\nVOLUME [\"/var/lib/beszel-agent\"]\n\nENTRYPOINT [\"/agent\"]"
  },
  {
    "path": "internal/dockerfile_agent_intel",
    "content": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n# Copy source files\nCOPY . ./\n\n# Build\nARG TARGETOS TARGETARCH\nRUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags \"-w -s\" -o /agent ./internal/cmd/agent\n\n# --------------------------\n# Final image\n# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume\n# --------------------------\nFROM alpine:3.23\n\nCOPY --from=builder /agent /agent\n\nRUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools smartmontools\n\n# Ensure data persistence across container recreations\nVOLUME [\"/var/lib/beszel-agent\"]\n\nENTRYPOINT [\"/agent\"]"
  },
  {
    "path": "internal/dockerfile_agent_nvidia",
    "content": "FROM --platform=$BUILDPLATFORM golang:bookworm AS builder\n\nWORKDIR /app\n\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n# Copy source files\nCOPY . ./\n\n# Build\nARG TARGETOS TARGETARCH\nRUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags glibc -ldflags \"-w -s\" -o /agent ./internal/cmd/agent\n\n# --------------------------\n# Smartmontools builder stage\n# --------------------------\nFROM nvidia/cuda:12.2.2-base-ubuntu22.04 AS smartmontools-builder\n\nRUN apt-get update && apt-get install -y \\\n  wget \\\n  build-essential \\\n  && wget https://downloads.sourceforge.net/project/smartmontools/smartmontools/7.5/smartmontools-7.5.tar.gz \\\n  && tar zxvf smartmontools-7.5.tar.gz \\\n  && cd smartmontools-7.5 \\\n  && ./configure --prefix=/usr --sysconfdir=/etc \\\n  && make \\\n  && make install \\\n  && rm -rf /smartmontools-7.5* \\\n  && apt-get remove -y wget build-essential \\\n  && apt-get autoremove -y \\\n  && rm -rf /var/lib/apt/lists/*\n\n# --------------------------\n# Final image: GPU-enabled agent with nvidia-smi\n# --------------------------\nFROM nvidia/cuda:12.2.2-base-ubuntu22.04\nCOPY --from=builder /agent /agent\n\n# AMD GPU name lookup (used by agent on hybrid laptops when /usr/share/libdrm/amdgpu.ids is read)\nCOPY --from=builder /app/agent/test-data/amdgpu.ids /usr/share/libdrm/amdgpu.ids\n\n# Copy smartmontools binaries and config files\nCOPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl\n\n# Ensure data persistence across container recreations\nVOLUME [\"/var/lib/beszel-agent\"]\n\nENTRYPOINT [\"/agent\"]\n"
  },
  {
    "path": "internal/dockerfile_hub",
    "content": "FROM --platform=$BUILDPLATFORM golang:alpine AS builder\n\nWORKDIR /app\n\n# Download Go modules\nCOPY ../go.mod ../go.sum ./\nRUN go mod download\n\n# Copy source files\nCOPY . ./\n\nRUN apk add --no-cache \\\n    unzip \\\n    ca-certificates\n\nRUN update-ca-certificates\n\n# Build\nARG TARGETOS TARGETARCH\nRUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags \"-w -s\" -o /beszel ./internal/cmd/hub\n\n# ? -------------------------\nFROM scratch\n\nCOPY --from=builder /beszel /\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\n# Ensure data persistence across container recreations\nVOLUME [\"/beszel_data\"]\n\nEXPOSE 8090\n\nENTRYPOINT [ \"/beszel\" ]\nCMD [\"serve\", \"--http=0.0.0.0:8090\"]"
  },
  {
    "path": "internal/entities/container/container.go",
    "content": "package container\n\nimport \"time\"\n\n// Docker container info from /containers/json\ntype ApiInfo struct {\n\tId      string\n\tIdShort string\n\tNames   []string\n\tStatus  string\n\tState   string\n\tImage   string\n\tHealth  struct {\n\t\tStatus string\n\t\t// FailingStreak int\n\t}\n\tPorts []struct {\n\t\t// PrivatePort uint16\n\t\tPublicPort uint16\n\t\tIP         string\n\t\t// Type        string\n\t}\n\t// ImageID string\n\t// Command string\n\t// Created int64\n\t// SizeRw     int64 `json:\",omitempty\"`\n\t// SizeRootFs int64 `json:\",omitempty\"`\n\t// Labels     map[string]string\n\t// HostConfig struct {\n\t// \tNetworkMode string            `json:\",omitempty\"`\n\t// \tAnnotations map[string]string `json:\",omitempty\"`\n\t// }\n\t// NetworkSettings *SummaryNetworkSettings\n\t// Mounts          []MountPoint\n}\n\n// Docker container resources from /containers/{id}/stats\ntype ApiStats struct {\n\tRead        time.Time `json:\"read\"`               // Time of stats generation\n\tNumProcs    uint32    `json:\"num_procs,omitzero\"` // Windows specific, not populated on Linux.\n\tNetworks    map[string]NetworkStats\n\tCPUStats    CPUStats    `json:\"cpu_stats\"`\n\tMemoryStats MemoryStats `json:\"memory_stats\"`\n}\n\n// Docker system info from /info API endpoint\ntype HostInfo struct {\n\tOperatingSystem string `json:\"OperatingSystem\"`\n\tKernelVersion   string `json:\"KernelVersion\"`\n\tNCPU            int    `json:\"NCPU\"`\n\tMemTotal        uint64 `json:\"MemTotal\"`\n}\n\nfunc (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {\n\tcpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer\n\tsystemDelta := s.CPUStats.SystemUsage - prevCpuSystem\n\n\t// Avoid division by zero and handle first run case\n\tif systemDelta == 0 || prevCpuContainer == 0 {\n\t\treturn 0.0\n\t}\n\n\treturn float64(cpuDelta) / float64(systemDelta) * 100.0\n}\n\n// from: https://github.com/docker/cli/blob/master/cli/command/container/stats_helpers.go#L185\nfunc (s *ApiStats) CalculateCpuPercentWindows(prevCpuUsage uint64, prevRead time.Time) float64 {\n\t// Max number of 100ns intervals between the previous time read and now\n\tpossIntervals := uint64(s.Read.Sub(prevRead).Nanoseconds())\n\tpossIntervals /= 100                // Convert to number of 100ns intervals\n\tpossIntervals *= uint64(s.NumProcs) // Multiple by the number of processors\n\n\t// Intervals used\n\tintervalsUsed := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage\n\n\t// Percentage avoiding divide-by-zero\n\tif possIntervals > 0 {\n\t\treturn float64(intervalsUsed) / float64(possIntervals) * 100.0\n\t}\n\treturn 0.00\n}\n\ntype CPUStats struct {\n\t// CPU Usage. Linux and Windows.\n\tCPUUsage CPUUsage `json:\"cpu_usage\"`\n\t// System Usage. Linux only.\n\tSystemUsage uint64 `json:\"system_cpu_usage,omitempty\"`\n}\n\ntype CPUUsage struct {\n\t// Total CPU time consumed.\n\t// Units: nanoseconds (Linux)\n\t// Units: 100's of nanoseconds (Windows)\n\tTotalUsage uint64 `json:\"total_usage\"`\n}\n\ntype MemoryStats struct {\n\t// current res_counter usage for memory\n\tUsage uint64 `json:\"usage,omitempty\"`\n\t// all the stats exported via memory.stat.\n\tStats MemoryStatsStats `json:\"stats\"`\n\t// private working set (Windows only)\n\tPrivateWorkingSet uint64 `json:\"privateworkingset,omitempty\"`\n}\n\ntype MemoryStatsStats struct {\n\tCache        uint64 `json:\"cache,omitempty\"`\n\tInactiveFile uint64 `json:\"inactive_file,omitempty\"`\n}\n\ntype NetworkStats struct {\n\t// Bytes received. Windows and Linux.\n\tRxBytes uint64 `json:\"rx_bytes\"`\n\t// Bytes sent. Windows and Linux.\n\tTxBytes uint64 `json:\"tx_bytes\"`\n}\n\ntype prevNetStats struct {\n\tSent uint64\n\tRecv uint64\n}\n\ntype DockerHealth = uint8\n\nconst (\n\tDockerHealthNone DockerHealth = iota\n\tDockerHealthStarting\n\tDockerHealthHealthy\n\tDockerHealthUnhealthy\n)\n\nvar DockerHealthStrings = map[string]DockerHealth{\n\t\"none\":      DockerHealthNone,\n\t\"starting\":  DockerHealthStarting,\n\t\"healthy\":   DockerHealthHealthy,\n\t\"unhealthy\": DockerHealthUnhealthy,\n}\n\n// Docker container stats\ntype Stats struct {\n\tName        string    `json:\"n\" cbor:\"0,keyasint\"`\n\tCpu         float64   `json:\"c\" cbor:\"1,keyasint\"`\n\tMem         float64   `json:\"m\" cbor:\"2,keyasint\"`\n\tNetworkSent float64   `json:\"ns,omitzero\" cbor:\"3,keyasint,omitzero\"` // deprecated 0.18.3 (MB) - keep field for old agents/records\n\tNetworkRecv float64   `json:\"nr,omitzero\" cbor:\"4,keyasint,omitzero\"` // deprecated 0.18.3 (MB) - keep field for old agents/records\n\tBandwidth   [2]uint64 `json:\"b,omitzero\" cbor:\"9,keyasint,omitzero\"`  // [sent bytes, recv bytes]\n\n\tHealth DockerHealth `json:\"-\" cbor:\"5,keyasint\"`\n\tStatus string       `json:\"-\" cbor:\"6,keyasint\"`\n\tId     string       `json:\"-\" cbor:\"7,keyasint\"`\n\tImage  string       `json:\"-\" cbor:\"8,keyasint\"`\n\tPorts  string       `json:\"-\" cbor:\"10,keyasint\"`\n\t// PrevCpu     [2]uint64    `json:\"-\"`\n\tCpuSystem    uint64       `json:\"-\"`\n\tCpuContainer uint64       `json:\"-\"`\n\tPrevNet      prevNetStats `json:\"-\"`\n\tPrevReadTime time.Time    `json:\"-\"`\n}\n"
  },
  {
    "path": "internal/entities/smart/smart.go",
    "content": "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 SmartctlInfo struct {\n\tVersion      VersionInfo `json:\"version\"`\n\tSvnRevision  string      `json:\"svn_revision\"`\n\tPlatformInfo string      `json:\"platform_info\"`\n\tBuildInfo    string      `json:\"build_info\"`\n\tArgv         []string    `json:\"argv\"`\n\tExitStatus   int         `json:\"exit_status\"`\n}\n\ntype DeviceInfo struct {\n\tName     string `json:\"name\"`\n\tInfoName string `json:\"info_name\"`\n\tType     string `json:\"type\"`\n\tProtocol string `json:\"protocol\"`\n}\n\ntype UserCapacity struct {\n\tBlocks uint64 `json:\"blocks\"`\n\tBytes  uint64 `json:\"bytes\"`\n}\n\n// type LocalTime struct {\n// \tTimeT   int64  `json:\"time_t\"`\n// \tAsctime string `json:\"asctime\"`\n// }\n\n// type WwnInfo struct {\n// \tNaa int `json:\"naa\"`\n// \tOui int `json:\"oui\"`\n// \tID  int `json:\"id\"`\n// }\n\n// type FormFactorInfo struct {\n// \tAtaValue int    `json:\"ata_value\"`\n// \tName     string `json:\"name\"`\n// }\n\n// type TrimInfo struct {\n// \tSupported bool `json:\"supported\"`\n// }\n\n// type AtaVersionInfo struct {\n// \tString     string `json:\"string\"`\n// \tMajorValue int    `json:\"major_value\"`\n// \tMinorValue int    `json:\"minor_value\"`\n// }\n\n// type VersionStringInfo struct {\n// \tString string `json:\"string\"`\n// \tValue  int    `json:\"value\"`\n// }\n\n// type SpeedInfo struct {\n// \tSataValue      int    `json:\"sata_value\"`\n// \tString         string `json:\"string\"`\n// \tUnitsPerSecond int    `json:\"units_per_second\"`\n// \tBitsPerUnit    int    `json:\"bits_per_unit\"`\n// }\n\n// type InterfaceSpeedInfo struct {\n// \tMax     SpeedInfo `json:\"max\"`\n// \tCurrent SpeedInfo `json:\"current\"`\n// }\n\ntype SmartStatusInfo struct {\n\tPassed bool `json:\"passed\"`\n}\n\ntype StatusInfo struct {\n\tValue  int    `json:\"value\"`\n\tString string `json:\"string\"`\n\tPassed bool   `json:\"passed\"`\n}\n\ntype PollingMinutes struct {\n\tShort    int `json:\"short\"`\n\tExtended int `json:\"extended\"`\n}\n\ntype CapabilitiesInfo struct {\n\tValues                        []int `json:\"values\"`\n\tExecOfflineImmediateSupported bool  `json:\"exec_offline_immediate_supported\"`\n\tOfflineIsAbortedUponNewCmd    bool  `json:\"offline_is_aborted_upon_new_cmd\"`\n\tOfflineSurfaceScanSupported   bool  `json:\"offline_surface_scan_supported\"`\n\tSelfTestsSupported            bool  `json:\"self_tests_supported\"`\n\tConveyanceSelfTestSupported   bool  `json:\"conveyance_self_test_supported\"`\n\tSelectiveSelfTestSupported    bool  `json:\"selective_self_test_supported\"`\n\tAttributeAutosaveEnabled      bool  `json:\"attribute_autosave_enabled\"`\n\tErrorLoggingSupported         bool  `json:\"error_logging_supported\"`\n\tGpLoggingSupported            bool  `json:\"gp_logging_supported\"`\n}\n\n// type AtaSmartData struct {\n// \tOfflineDataCollection OfflineDataCollectionInfo `json:\"offline_data_collection\"`\n// \tSelfTest              SelfTestInfo              `json:\"self_test\"`\n// \tCapabilities          CapabilitiesInfo          `json:\"capabilities\"`\n// }\n\n// type OfflineDataCollectionInfo struct {\n// \tStatus            StatusInfo `json:\"status\"`\n// \tCompletionSeconds int        `json:\"completion_seconds\"`\n// }\n\n// type SelfTestInfo struct {\n// \tStatus         StatusInfo     `json:\"status\"`\n// \tPollingMinutes PollingMinutes `json:\"polling_minutes\"`\n// }\n\n// type AtaSctCapabilities struct {\n// \tValue                         int  `json:\"value\"`\n// \tErrorRecoveryControlSupported bool `json:\"error_recovery_control_supported\"`\n// \tFeatureControlSupported       bool `json:\"feature_control_supported\"`\n// \tDataTableSupported            bool `json:\"data_table_supported\"`\n// }\n\ntype SummaryInfo struct {\n\tRevision int `json:\"revision\"`\n\tCount    int `json:\"count\"`\n}\n\ntype AtaSmartAttributes struct {\n\tTable []AtaSmartAttribute `json:\"table\"`\n}\n\ntype AtaDeviceStatistics struct {\n\tPages []AtaDeviceStatisticsPage `json:\"pages\"`\n}\n\ntype AtaDeviceStatisticsPage struct {\n\tNumber uint8                      `json:\"number\"`\n\tTable  []AtaDeviceStatisticsEntry `json:\"table\"`\n}\n\ntype AtaDeviceStatisticsEntry struct {\n\tName  string `json:\"name\"`\n\tValue *int64 `json:\"value,omitempty\"`\n}\n\ntype AtaSmartAttribute struct {\n\tID         uint16 `json:\"id\"`\n\tName       string `json:\"name\"`\n\tValue      uint16 `json:\"value\"`\n\tWorst      uint16 `json:\"worst\"`\n\tThresh     uint16 `json:\"thresh\"`\n\tWhenFailed string `json:\"when_failed\"`\n\t// Flags      AttributeFlags `json:\"flags\"`\n\tRaw RawValue `json:\"raw\"`\n}\n\n// type AttributeFlags struct {\n// \tValue         int    `json:\"value\"`\n// \tString        string `json:\"string\"`\n// \tPrefailure    bool   `json:\"prefailure\"`\n// \tUpdatedOnline bool   `json:\"updated_online\"`\n// \tPerformance   bool   `json:\"performance\"`\n// \tErrorRate     bool   `json:\"error_rate\"`\n// \tEventCount    bool   `json:\"event_count\"`\n// \tAutoKeep      bool   `json:\"auto_keep\"`\n// }\n\ntype RawValue struct {\n\tValue  SmartRawValue `json:\"value\"`\n\tString string        `json:\"string\"`\n}\n\nfunc (r *RawValue) UnmarshalJSON(data []byte) error {\n\tvar tmp struct {\n\t\tValue  json.RawMessage `json:\"value\"`\n\t\tString string          `json:\"string\"`\n\t}\n\n\tif err := json.Unmarshal(data, &tmp); err != nil {\n\t\treturn err\n\t}\n\n\tif len(tmp.Value) > 0 {\n\t\tif err := r.Value.UnmarshalJSON(tmp.Value); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tr.Value = 0\n\t}\n\n\tr.String = tmp.String\n\n\tif parsed, ok := ParseSmartRawValueString(tmp.String); ok {\n\t\tr.Value = SmartRawValue(parsed)\n\t}\n\n\treturn nil\n}\n\ntype SmartRawValue uint64\n\n// handles when drives report strings like \"0h+0m+0.000s\" or \"7344 (253d 8h)\" for power on hours\nfunc (v *SmartRawValue) UnmarshalJSON(data []byte) error {\n\ttrimmed := strings.TrimSpace(string(data))\n\tif len(trimmed) == 0 || trimmed == \"null\" {\n\t\t*v = 0\n\t\treturn nil\n\t}\n\n\tif trimmed[0] == '\"' {\n\t\tvalueStr, err := strconv.Unquote(trimmed)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tparsed, ok := ParseSmartRawValueString(valueStr)\n\t\tif ok {\n\t\t\t*v = SmartRawValue(parsed)\n\t\t\treturn nil\n\t\t}\n\t\t*v = 0\n\t\treturn nil\n\t}\n\n\tif parsed, err := strconv.ParseUint(trimmed, 0, 64); err == nil {\n\t\t*v = SmartRawValue(parsed)\n\t\treturn nil\n\t}\n\n\tif parsed, ok := ParseSmartRawValueString(trimmed); ok {\n\t\t*v = SmartRawValue(parsed)\n\t\treturn nil\n\t}\n\n\t*v = 0\n\treturn nil\n}\n\n// ParseSmartRawValueString attempts to extract a numeric value from the raw value\n// strings emitted by smartctl, which sometimes include human-friendly annotations\n// like \"7344 (253d 8h)\" or \"0h+0m+0.000s\". It returns the parsed value and a\n// boolean indicating success.\nfunc ParseSmartRawValueString(value string) (uint64, bool) {\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn 0, false\n\t}\n\n\tif parsed, err := strconv.ParseUint(value, 0, 64); err == nil {\n\t\treturn parsed, true\n\t}\n\n\tif idx := strings.IndexRune(value, 'h'); idx > 0 {\n\t\thoursPart := strings.TrimSpace(value[:idx])\n\t\tif hoursPart != \"\" {\n\t\t\tif parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {\n\t\t\t\treturn uint64(parsed), true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := 0; i < len(value); i++ {\n\t\tif value[i] < '0' || value[i] > '9' {\n\t\t\tcontinue\n\t\t}\n\t\tend := i + 1\n\t\tfor end < len(value) && value[end] >= '0' && value[end] <= '9' {\n\t\t\tend++\n\t\t}\n\t\tdigits := value[i:end]\n\t\tif parsed, err := strconv.ParseUint(digits, 10, 64); err == nil {\n\t\t\treturn parsed, true\n\t\t}\n\t\ti = end\n\t}\n\n\treturn 0, false\n}\n\n// type PowerOnTimeInfo struct {\n// \tHours uint32 `json:\"hours\"`\n// }\n\ntype TemperatureInfo struct {\n\tCurrent uint8 `json:\"current\"`\n}\n\ntype TemperatureInfoScsi struct {\n\tCurrent   uint8 `json:\"current\"`\n\tDriveTrip uint8 `json:\"drive_trip\"`\n}\n\n// type SelectiveSelfTestTable struct {\n// \tLbaMin int        `json:\"lba_min\"`\n// \tLbaMax int        `json:\"lba_max\"`\n// \tStatus StatusInfo `json:\"status\"`\n// }\n\n// type SelectiveSelfTestFlags struct {\n// \tValue                int  `json:\"value\"`\n// \tRemainderScanEnabled bool `json:\"remainder_scan_enabled\"`\n// }\n\n// type AtaSmartSelectiveSelfTestLog struct {\n// \tRevision                 int                      `json:\"revision\"`\n// \tTable                    []SelectiveSelfTestTable `json:\"table\"`\n// \tFlags                    SelectiveSelfTestFlags   `json:\"flags\"`\n// \tPowerUpScanResumeMinutes int                      `json:\"power_up_scan_resume_minutes\"`\n// }\n\n// BaseSmartInfo contains common fields shared between SATA and NVMe drives\n// type BaseSmartInfo struct {\n// \tDevice           DeviceInfo   `json:\"device\"`\n// \tModelName        string       `json:\"model_name\"`\n// \tSerialNumber     string       `json:\"serial_number\"`\n// \tFirmwareVersion  string       `json:\"firmware_version\"`\n// \tUserCapacity     UserCapacity `json:\"user_capacity\"`\n// \tLogicalBlockSize int          `json:\"logical_block_size\"`\n// \tLocalTime        LocalTime    `json:\"local_time\"`\n// }\n\ntype SmartctlInfoLegacy struct {\n\tVersion      VersionInfo `json:\"version\"`\n\tSvnRevision  string      `json:\"svn_revision\"`\n\tPlatformInfo string      `json:\"platform_info\"`\n\tBuildInfo    string      `json:\"build_info\"`\n\tArgv         []string    `json:\"argv\"`\n\tExitStatus   int         `json:\"exit_status\"`\n}\n\ntype SmartInfoForSata struct {\n\t// JSONFormatVersion VersionInfo        `json:\"json_format_version\"`\n\tSmartctl SmartctlInfoLegacy `json:\"smartctl\"`\n\tDevice   DeviceInfo         `json:\"device\"`\n\t// ModelFamily  string             `json:\"model_family\"`\n\tModelName    string `json:\"model_name\"`\n\tSerialNumber string `json:\"serial_number\"`\n\t// Wwn               WwnInfo            `json:\"wwn\"`\n\tFirmwareVersion string       `json:\"firmware_version\"`\n\tUserCapacity    UserCapacity `json:\"user_capacity\"`\n\tScsiVendor      string       `json:\"scsi_vendor\"`\n\tScsiProduct     string       `json:\"scsi_product\"`\n\t// LogicalBlockSize  int                `json:\"logical_block_size\"`\n\t// PhysicalBlockSize int                `json:\"physical_block_size\"`\n\t// RotationRate      int                `json:\"rotation_rate\"`\n\t// FormFactor        FormFactorInfo     `json:\"form_factor\"`\n\t// Trim                         TrimInfo                     `json:\"trim\"`\n\t// InSmartctlDatabase           bool                         `json:\"in_smartctl_database\"`\n\t// AtaVersion                   AtaVersionInfo               `json:\"ata_version\"`\n\t// SataVersion                  VersionStringInfo            `json:\"sata_version\"`\n\t// InterfaceSpeed               InterfaceSpeedInfo           `json:\"interface_speed\"`\n\t// LocalTime                    LocalTime                    `json:\"local_time\"`\n\tSmartStatus SmartStatusInfo `json:\"smart_status\"`\n\t// AtaSmartData                 AtaSmartData                 `json:\"ata_smart_data\"`\n\t// AtaSctCapabilities           AtaSctCapabilities           `json:\"ata_sct_capabilities\"`\n\tAtaSmartAttributes  AtaSmartAttributes `json:\"ata_smart_attributes\"`\n\tAtaDeviceStatistics json.RawMessage    `json:\"ata_device_statistics\"`\n\t// PowerOnTime                  PowerOnTimeInfo              `json:\"power_on_time\"`\n\t// PowerCycleCount              uint16                       `json:\"power_cycle_count\"`\n\tTemperature TemperatureInfo `json:\"temperature\"`\n\t// AtaSmartErrorLog             AtaSmartErrorLog             `json:\"ata_smart_error_log\"`\n\t// AtaSmartSelfTestLog          AtaSmartSelfTestLog          `json:\"ata_smart_self_test_log\"`\n\t// AtaSmartSelectiveSelfTestLog AtaSmartSelectiveSelfTestLog `json:\"ata_smart_selective_self_test_log\"`\n}\n\ntype ScsiErrorCounter struct {\n\tErrorsCorrectedByECCFast         uint64 `json:\"errors_corrected_by_eccfast\"`\n\tErrorsCorrectedByECCDelayed      uint64 `json:\"errors_corrected_by_eccdelayed\"`\n\tErrorsCorrectedByRereadsRewrites uint64 `json:\"errors_corrected_by_rereads_rewrites\"`\n\tTotalErrorsCorrected             uint64 `json:\"total_errors_corrected\"`\n\tCorrectionAlgorithmInvocations   uint64 `json:\"correction_algorithm_invocations\"`\n\tGigabytesProcessed               string `json:\"gigabytes_processed\"`\n\tTotalUncorrectedErrors           uint64 `json:\"total_uncorrected_errors\"`\n}\n\ntype ScsiErrorCounterLog struct {\n\tRead   ScsiErrorCounter `json:\"read\"`\n\tWrite  ScsiErrorCounter `json:\"write\"`\n\tVerify ScsiErrorCounter `json:\"verify\"`\n}\n\ntype ScsiStartStopCycleCounter struct {\n\tYearOfManufacture                          string `json:\"year_of_manufacture\"`\n\tWeekOfManufacture                          string `json:\"week_of_manufacture\"`\n\tSpecifiedCycleCountOverDeviceLifetime      uint64 `json:\"specified_cycle_count_over_device_lifetime\"`\n\tAccumulatedStartStopCycles                 uint64 `json:\"accumulated_start_stop_cycles\"`\n\tSpecifiedLoadUnloadCountOverDeviceLifetime uint64 `json:\"specified_load_unload_count_over_device_lifetime\"`\n\tAccumulatedLoadUnloadCycles                uint64 `json:\"accumulated_load_unload_cycles\"`\n}\n\ntype PowerOnTimeScsi struct {\n\tHours   uint64 `json:\"hours\"`\n\tMinutes uint64 `json:\"minutes\"`\n}\n\ntype SmartInfoForScsi struct {\n\tSmartctl                  SmartctlInfoLegacy        `json:\"smartctl\"`\n\tDevice                    DeviceInfo                `json:\"device\"`\n\tScsiVendor                string                    `json:\"scsi_vendor\"`\n\tScsiProduct               string                    `json:\"scsi_product\"`\n\tScsiModelName             string                    `json:\"scsi_model_name\"`\n\tScsiRevision              string                    `json:\"scsi_revision\"`\n\tScsiVersion               string                    `json:\"scsi_version\"`\n\tSerialNumber              string                    `json:\"serial_number\"`\n\tUserCapacity              UserCapacity              `json:\"user_capacity\"`\n\tTemperature               TemperatureInfoScsi       `json:\"temperature\"`\n\tSmartStatus               SmartStatusInfo           `json:\"smart_status\"`\n\tPowerOnTime               PowerOnTimeScsi           `json:\"power_on_time\"`\n\tScsiStartStopCycleCounter ScsiStartStopCycleCounter `json:\"scsi_start_stop_cycle_counter\"`\n\tScsiGrownDefectList       uint64                    `json:\"scsi_grown_defect_list\"`\n\tScsiErrorCounterLog       ScsiErrorCounterLog       `json:\"scsi_error_counter_log\"`\n}\n\n// type AtaSmartErrorLog struct {\n// \tSummary SummaryInfo `json:\"summary\"`\n// }\n\n// type AtaSmartSelfTestLog struct {\n// \tStandard SummaryInfo `json:\"standard\"`\n// }\n\ntype SmartctlInfoNvme struct {\n\tVersion      VersionInfo `json:\"version\"`\n\tSVNRevision  string      `json:\"svn_revision\"`\n\tPlatformInfo string      `json:\"platform_info\"`\n\tBuildInfo    string      `json:\"build_info\"`\n\tArgv         []string    `json:\"argv\"`\n\tExitStatus   int         `json:\"exit_status\"`\n}\n\n// type NVMePCIVendor struct {\n// \tID          int `json:\"id\"`\n// \tSubsystemID int `json:\"subsystem_id\"`\n// }\n\n// type SizeCapacityInfo struct {\n// \tBlocks uint64 `json:\"blocks\"`\n// \tBytes  uint64 `json:\"bytes\"`\n// }\n\n// type EUI64Info struct {\n// \tOUI   int `json:\"oui\"`\n// \tExtID int `json:\"ext_id\"`\n// }\n\n// type NVMeNamespace struct {\n// \tID               uint32           `json:\"id\"`\n// \tSize             SizeCapacityInfo `json:\"size\"`\n// \tCapacity         SizeCapacityInfo `json:\"capacity\"`\n// \tUtilization      SizeCapacityInfo `json:\"utilization\"`\n// \tFormattedLBASize uint32           `json:\"formatted_lba_size\"`\n// \tEUI64            EUI64Info        `json:\"eui64\"`\n// }\n\ntype SmartStatusInfoNvme struct {\n\tPassed bool            `json:\"passed\"`\n\tNVMe   SmartStatusNVMe `json:\"nvme\"`\n}\n\ntype SmartStatusNVMe struct {\n\tValue int `json:\"value\"`\n}\n\ntype NVMeSmartHealthInformationLog struct {\n\tCriticalWarning         uint    `json:\"critical_warning\"`\n\tTemperature             uint8   `json:\"temperature\"`\n\tAvailableSpare          uint    `json:\"available_spare\"`\n\tAvailableSpareThreshold uint    `json:\"available_spare_threshold\"`\n\tPercentageUsed          uint8   `json:\"percentage_used\"`\n\tDataUnitsRead           uint64  `json:\"data_units_read\"`\n\tDataUnitsWritten        uint64  `json:\"data_units_written\"`\n\tHostReads               uint    `json:\"host_reads\"`\n\tHostWrites              uint    `json:\"host_writes\"`\n\tControllerBusyTime      uint    `json:\"controller_busy_time\"`\n\tPowerCycles             uint16  `json:\"power_cycles\"`\n\tPowerOnHours            uint32  `json:\"power_on_hours\"`\n\tUnsafeShutdowns         uint16  `json:\"unsafe_shutdowns\"`\n\tMediaErrors             uint    `json:\"media_errors\"`\n\tNumErrLogEntries        uint    `json:\"num_err_log_entries\"`\n\tWarningTempTime         uint    `json:\"warning_temp_time\"`\n\tCriticalCompTime        uint    `json:\"critical_comp_time\"`\n\tTemperatureSensors      []uint8 `json:\"temperature_sensors\"`\n}\n\ntype SmartInfoForNvme struct {\n\t// JSONFormatVersion             VersionInfo                   `json:\"json_format_version\"`\n\tSmartctl        SmartctlInfoNvme `json:\"smartctl\"`\n\tDevice          DeviceInfo       `json:\"device\"`\n\tModelName       string           `json:\"model_name\"`\n\tSerialNumber    string           `json:\"serial_number\"`\n\tFirmwareVersion string           `json:\"firmware_version\"`\n\t// NVMePCIVendor                 NVMePCIVendor                 `json:\"nvme_pci_vendor\"`\n\t// NVMeIEEEOUIIdentifier         uint32                        `json:\"nvme_ieee_oui_identifier\"`\n\t// NVMeTotalCapacity             uint64                        `json:\"nvme_total_capacity\"`\n\t// NVMeUnallocatedCapacity       uint64                        `json:\"nvme_unallocated_capacity\"`\n\t// NVMeControllerID              uint16                        `json:\"nvme_controller_id\"`\n\t// NVMeVersion                   VersionStringInfo             `json:\"nvme_version\"`\n\t// NVMeNumberOfNamespaces        uint8                         `json:\"nvme_number_of_namespaces\"`\n\t// NVMeNamespaces                []NVMeNamespace               `json:\"nvme_namespaces\"`\n\tUserCapacity UserCapacity `json:\"user_capacity\"`\n\t// LogicalBlockSize              int                           `json:\"logical_block_size\"`\n\t// LocalTime                     LocalTime                     `json:\"local_time\"`\n\tSmartStatus                   SmartStatusInfoNvme           `json:\"smart_status\"`\n\tNVMeSmartHealthInformationLog NVMeSmartHealthInformationLog `json:\"nvme_smart_health_information_log\"`\n\tTemperature                   TemperatureInfoNvme           `json:\"temperature\"`\n\tPowerCycleCount               uint16                        `json:\"power_cycle_count\"`\n\tPowerOnTime                   PowerOnTimeInfoNvme           `json:\"power_on_time\"`\n}\n\ntype TemperatureInfoNvme struct {\n\tCurrent int `json:\"current\"`\n}\n\ntype PowerOnTimeInfoNvme struct {\n\tHours int `json:\"hours\"`\n}\n\ntype SmartData struct {\n\t// ModelFamily     string            `json:\"mf,omitempty\" cbor:\"0,keyasint,omitempty\"`\n\tModelName       string            `json:\"mn,omitempty\" cbor:\"1,keyasint,omitempty\"`\n\tSerialNumber    string            `json:\"sn,omitempty\" cbor:\"2,keyasint,omitempty\"`\n\tFirmwareVersion string            `json:\"fv,omitempty\" cbor:\"3,keyasint,omitempty\"`\n\tCapacity        uint64            `json:\"c,omitempty\" cbor:\"4,keyasint,omitempty\"`\n\tSmartStatus     string            `json:\"s,omitempty\" cbor:\"5,keyasint,omitempty\"`\n\tDiskName        string            `json:\"dn,omitempty\" cbor:\"6,keyasint,omitempty\"`\n\tDiskType        string            `json:\"dt,omitempty\" cbor:\"7,keyasint,omitempty\"`\n\tTemperature     uint8             `json:\"t,omitempty\" cbor:\"8,keyasint,omitempty\"`\n\tAttributes      []*SmartAttribute `json:\"a,omitempty\" cbor:\"9,keyasint,omitempty\"`\n}\n\ntype SmartAttribute struct {\n\tID         uint16 `json:\"id,omitempty\" cbor:\"0,keyasint,omitempty\"`\n\tName       string `json:\"n\" cbor:\"1,keyasint\"`\n\tValue      uint16 `json:\"v,omitempty\" cbor:\"2,keyasint,omitempty\"`\n\tWorst      uint16 `json:\"w,omitempty\" cbor:\"3,keyasint,omitempty\"`\n\tThreshold  uint16 `json:\"t,omitempty\" cbor:\"4,keyasint,omitempty\"`\n\tRawValue   uint64 `json:\"rv\" cbor:\"5,keyasint\"`\n\tRawString  string `json:\"rs,omitempty\" cbor:\"6,keyasint,omitempty\"`\n\tWhenFailed string `json:\"wf,omitempty\" cbor:\"7,keyasint,omitempty\"`\n}\n"
  },
  {
    "path": "internal/entities/smart/smart_test.go",
    "content": "package smart\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSmartRawValueUnmarshalDuration(t *testing.T) {\n\tinput := []byte(`{\"value\":\"62312h+33m+50.907s\",\"string\":\"62312h+33m+50.907s\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 62312, raw.Value)\n}\n\nfunc TestSmartRawValueUnmarshalNumericString(t *testing.T) {\n\tinput := []byte(`{\"value\":\"7344\",\"string\":\"7344\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 7344, raw.Value)\n}\n\nfunc TestSmartRawValueUnmarshalParenthetical(t *testing.T) {\n\tinput := []byte(`{\"value\":\"39925 (212 206 0)\",\"string\":\"39925 (212 206 0)\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 39925, raw.Value)\n}\n\nfunc TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {\n\tinput := []byte(`{\"value\":\"2748h+31m+49.560s\",\"string\":\"2748h+31m+49.560s\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 2748, raw.Value)\n}\n\nfunc TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {\n\tinput := []byte(`{\"value\":57891864217128,\"string\":\"39925 (212 206 0)\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 39925, raw.Value)\n}\n\nfunc TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {\n\tinput := []byte(`{\"value\":57891864217128,\"string\":\"2748h+31m+49.560s\"}`)\n\tvar raw RawValue\n\terr := json.Unmarshal(input, &raw)\n\tassert.NoError(t, err)\n\n\tassert.EqualValues(t, 2748, raw.Value)\n}\n"
  },
  {
    "path": "internal/entities/system/system.go",
    "content": "package system\n\n// TODO: this is confusing, make common package with common/types common/helpers etc\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\ntype Stats struct {\n\tCpu            float64             `json:\"cpu\" cbor:\"0,keyasint\"`\n\tMaxCpu         float64             `json:\"cpum,omitempty\" cbor:\"-\"`\n\tMem            float64             `json:\"m\" cbor:\"2,keyasint\"`\n\tMaxMem         float64             `json:\"mm,omitempty\" cbor:\"-\"`\n\tMemUsed        float64             `json:\"mu\" cbor:\"3,keyasint\"`\n\tMemPct         float64             `json:\"mp\" cbor:\"4,keyasint\"`\n\tMemBuffCache   float64             `json:\"mb\" cbor:\"5,keyasint\"`\n\tMemZfsArc      float64             `json:\"mz,omitempty\" cbor:\"6,keyasint,omitempty\"` // ZFS ARC memory\n\tSwap           float64             `json:\"s,omitempty\" cbor:\"7,keyasint,omitempty\"`\n\tSwapUsed       float64             `json:\"su,omitempty\" cbor:\"8,keyasint,omitempty\"`\n\tDiskTotal      float64             `json:\"d\" cbor:\"9,keyasint\"`\n\tDiskUsed       float64             `json:\"du\" cbor:\"10,keyasint\"`\n\tDiskPct        float64             `json:\"dp\" cbor:\"11,keyasint\"`\n\tDiskReadPs     float64             `json:\"dr,omitzero\" cbor:\"12,keyasint,omitzero\"`\n\tDiskWritePs    float64             `json:\"dw,omitzero\" cbor:\"13,keyasint,omitzero\"`\n\tMaxDiskReadPs  float64             `json:\"drm,omitempty\" cbor:\"-\"`\n\tMaxDiskWritePs float64             `json:\"dwm,omitempty\" cbor:\"-\"`\n\tNetworkSent    float64             `json:\"ns,omitzero\" cbor:\"16,keyasint,omitzero\"`\n\tNetworkRecv    float64             `json:\"nr,omitzero\" cbor:\"17,keyasint,omitzero\"`\n\tMaxNetworkSent float64             `json:\"nsm,omitempty\" cbor:\"-\"`\n\tMaxNetworkRecv float64             `json:\"nrm,omitempty\" cbor:\"-\"`\n\tTemperatures   map[string]float64  `json:\"t,omitempty\" cbor:\"20,keyasint,omitempty\"`\n\tExtraFs        map[string]*FsStats `json:\"efs,omitempty\" cbor:\"21,keyasint,omitempty\"`\n\tGPUData        map[string]GPUData  `json:\"g,omitempty\" cbor:\"22,keyasint,omitempty\"`\n\t// LoadAvg1       float64             `json:\"l1,omitempty\" cbor:\"23,keyasint,omitempty\"`\n\t// LoadAvg5       float64             `json:\"l5,omitempty\" cbor:\"24,keyasint,omitempty\"`\n\t// LoadAvg15      float64             `json:\"l15,omitempty\" cbor:\"25,keyasint,omitempty\"`\n\tBandwidth    [2]uint64 `json:\"b,omitzero\" cbor:\"26,keyasint,omitzero\"` // [sent bytes, recv bytes]\n\tMaxBandwidth [2]uint64 `json:\"bm,omitzero\" cbor:\"-\"`                   // [sent bytes, recv bytes]\n\t// TODO: remove other load fields in future release in favor of load avg array\n\tLoadAvg           [3]float64           `json:\"la,omitempty\" cbor:\"28,keyasint\"`\n\tBattery           [2]uint8             `json:\"bat,omitzero\" cbor:\"29,keyasint,omitzero\"`    // [percent, charge state, current]\n\tNetworkInterfaces map[string][4]uint64 `json:\"ni,omitempty\" cbor:\"31,keyasint,omitempty\"`   // [upload bytes, download bytes, total upload, total download]\n\tDiskIO            [2]uint64            `json:\"dio,omitzero\" cbor:\"32,keyasint,omitzero\"`    // [read bytes, write bytes]\n\tMaxDiskIO         [2]uint64            `json:\"diom,omitzero\" cbor:\"-\"`                      // [max read bytes, max write bytes]\n\tCpuBreakdown      []float64            `json:\"cpub,omitempty\" cbor:\"33,keyasint,omitempty\"` // [user, system, iowait, steal, idle]\n\tCpuCoresUsage     Uint8Slice           `json:\"cpus,omitempty\" cbor:\"34,keyasint,omitempty\"` // per-core busy usage [CPU0..]\n}\n\n// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.\n// JSON: encodes as array of numbers (avoids base64 string).\n// CBOR: falls back to default handling for []uint8 (byte string), keeping payload small.\ntype Uint8Slice []uint8\n\nfunc (s Uint8Slice) MarshalJSON() ([]byte, error) {\n\tif s == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\t// Convert to wider ints to force array-of-numbers encoding.\n\tarr := make([]uint16, len(s))\n\tfor i, v := range s {\n\t\tarr[i] = uint16(v)\n\t}\n\treturn json.Marshal(arr)\n}\n\ntype GPUData struct {\n\tName        string             `json:\"n\" cbor:\"0,keyasint\"`\n\tTemperature float64            `json:\"-\"`\n\tMemoryUsed  float64            `json:\"mu,omitempty,omitzero\" cbor:\"1,keyasint,omitempty,omitzero\"`\n\tMemoryTotal float64            `json:\"mt,omitempty,omitzero\" cbor:\"2,keyasint,omitempty,omitzero\"`\n\tUsage       float64            `json:\"u\" cbor:\"3,keyasint,omitempty\"`\n\tPower       float64            `json:\"p,omitempty\" cbor:\"4,keyasint,omitempty\"`\n\tCount       float64            `json:\"-\"`\n\tEngines     map[string]float64 `json:\"e,omitempty\" cbor:\"5,keyasint,omitempty\"`\n\tPowerPkg    float64            `json:\"pp,omitempty\" cbor:\"6,keyasint,omitempty\"`\n}\n\ntype FsStats struct {\n\tTime           time.Time `json:\"-\"`\n\tRoot           bool      `json:\"-\"`\n\tMountpoint     string    `json:\"-\"`\n\tName           string    `json:\"-\"`\n\tDiskTotal      float64   `json:\"d\" cbor:\"0,keyasint\"`\n\tDiskUsed       float64   `json:\"du\" cbor:\"1,keyasint\"`\n\tTotalRead      uint64    `json:\"-\"`\n\tTotalWrite     uint64    `json:\"-\"`\n\tDiskReadPs     float64   `json:\"r\" cbor:\"2,keyasint\"`\n\tDiskWritePs    float64   `json:\"w\" cbor:\"3,keyasint\"`\n\tMaxDiskReadPS  float64   `json:\"rm,omitempty\" cbor:\"-\"`\n\tMaxDiskWritePS float64   `json:\"wm,omitempty\" cbor:\"-\"`\n\t// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes\n\tDiskReadBytes     uint64 `json:\"rb\" cbor:\"6,keyasint,omitempty\"`\n\tDiskWriteBytes    uint64 `json:\"wb\" cbor:\"7,keyasint,omitempty\"`\n\tMaxDiskReadBytes  uint64 `json:\"rbm,omitempty\" cbor:\"-\"`\n\tMaxDiskWriteBytes uint64 `json:\"wbm,omitempty\" cbor:\"-\"`\n}\n\ntype NetIoStats struct {\n\tBytesRecv uint64\n\tBytesSent uint64\n\tTime      time.Time\n\tName      string\n}\n\ntype Os = uint8\n\nconst (\n\tLinux Os = iota\n\tDarwin\n\tWindows\n\tFreebsd\n)\n\ntype ConnectionType = uint8\n\nconst (\n\tConnectionTypeNone ConnectionType = iota\n\tConnectionTypeSSH\n\tConnectionTypeWebSocket\n)\n\n// Core system data that is needed in All Systems table\ntype Info struct {\n\tHostname      string `json:\"h,omitempty\" cbor:\"0,keyasint,omitempty\"` // deprecated - moved to Details struct\n\tKernelVersion string `json:\"k,omitempty\" cbor:\"1,keyasint,omitempty\"` // deprecated - moved to Details struct\n\tCores         int    `json:\"c,omitzero\" cbor:\"2,keyasint,omitzero\"`   // deprecated - moved to Details struct\n\t// Threads is needed in Info struct to calculate load average thresholds\n\tThreads       int     `json:\"t,omitempty\" cbor:\"3,keyasint,omitempty\"`\n\tCpuModel      string  `json:\"m,omitempty\" cbor:\"4,keyasint,omitempty\"` // deprecated - moved to Details struct\n\tUptime        uint64  `json:\"u\" cbor:\"5,keyasint\"`\n\tCpu           float64 `json:\"cpu\" cbor:\"6,keyasint\"`\n\tMemPct        float64 `json:\"mp\" cbor:\"7,keyasint\"`\n\tDiskPct       float64 `json:\"dp\" cbor:\"8,keyasint\"`\n\tBandwidth     float64 `json:\"b,omitzero\" cbor:\"9,keyasint\"` // deprecated in favor of BandwidthBytes\n\tAgentVersion  string  `json:\"v\" cbor:\"10,keyasint\"`\n\tPodman        bool    `json:\"p,omitempty\" cbor:\"11,keyasint,omitempty\"` // deprecated - moved to Details struct\n\tGpuPct        float64 `json:\"g,omitempty\" cbor:\"12,keyasint,omitempty\"`\n\tDashboardTemp float64 `json:\"dt,omitempty\" cbor:\"13,keyasint,omitempty\"`\n\tOs            Os      `json:\"os,omitempty\" cbor:\"14,keyasint,omitempty\"` // deprecated - moved to Details struct\n\t// LoadAvg1       float64 `json:\"l1,omitempty\" cbor:\"15,keyasint,omitempty\"`  // deprecated - use `la` array instead\n\t// LoadAvg5       float64 `json:\"l5,omitempty\" cbor:\"16,keyasint,omitempty\"`  // deprecated - use `la` array instead\n\t// LoadAvg15      float64 `json:\"l15,omitempty\" cbor:\"17,keyasint,omitempty\"` // deprecated - use `la` array instead\n\n\tBandwidthBytes uint64             `json:\"bb\" cbor:\"18,keyasint\"`\n\tLoadAvg        [3]float64         `json:\"la,omitempty\" cbor:\"19,keyasint\"`\n\tConnectionType ConnectionType     `json:\"ct,omitempty\" cbor:\"20,keyasint,omitempty,omitzero\"`\n\tExtraFsPct     map[string]float64 `json:\"efs,omitempty\" cbor:\"21,keyasint,omitempty\"`\n\tServices       []uint16           `json:\"sv,omitempty\" cbor:\"22,keyasint,omitempty\"` // [totalServices, numFailedServices]\n\tBattery        [2]uint8           `json:\"bat,omitzero\" cbor:\"23,keyasint,omitzero\"`  // [percent, charge state]\n}\n\n// Data that does not change during process lifetime and is not needed in All Systems table\ntype Details struct {\n\tHostname      string        `cbor:\"0,keyasint\"`\n\tKernel        string        `cbor:\"1,keyasint,omitempty\"`\n\tCores         int           `cbor:\"2,keyasint\"`\n\tThreads       int           `cbor:\"3,keyasint\"`\n\tCpuModel      string        `cbor:\"4,keyasint\"`\n\tOs            Os            `cbor:\"5,keyasint\"`\n\tOsName        string        `cbor:\"6,keyasint\"`\n\tArch          string        `cbor:\"7,keyasint\"`\n\tPodman        bool          `cbor:\"8,keyasint,omitempty\"`\n\tMemoryTotal   uint64        `cbor:\"9,keyasint\"`\n\tSmartInterval time.Duration `cbor:\"10,keyasint,omitempty\"`\n}\n\n// Final data structure to return to the hub\ntype CombinedData struct {\n\tStats           Stats              `json:\"stats\" cbor:\"0,keyasint\"`\n\tInfo            Info               `json:\"info\" cbor:\"1,keyasint\"`\n\tContainers      []*container.Stats `json:\"container\" cbor:\"2,keyasint\"`\n\tSystemdServices []*systemd.Service `json:\"systemd,omitempty\" cbor:\"3,keyasint,omitempty\"`\n\tDetails         *Details           `cbor:\"4,keyasint,omitempty\"`\n}\n"
  },
  {
    "path": "internal/entities/systemd/systemd.go",
    "content": "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 ServiceState uint8\n\nconst (\n\tStatusActive ServiceState = iota\n\tStatusInactive\n\tStatusFailed\n\tStatusActivating\n\tStatusDeactivating\n\tStatusReloading\n)\n\n// ServiceSubState represents the sub status of a systemd service\ntype ServiceSubState uint8\n\nconst (\n\tSubStateDead ServiceSubState = iota\n\tSubStateRunning\n\tSubStateExited\n\tSubStateFailed\n\tSubStateUnknown\n)\n\n// ParseServiceStatus converts a string status to a ServiceStatus enum value\nfunc ParseServiceStatus(status string) ServiceState {\n\tswitch status {\n\tcase \"active\":\n\t\treturn StatusActive\n\tcase \"inactive\":\n\t\treturn StatusInactive\n\tcase \"failed\":\n\t\treturn StatusFailed\n\tcase \"activating\":\n\t\treturn StatusActivating\n\tcase \"deactivating\":\n\t\treturn StatusDeactivating\n\tcase \"reloading\":\n\t\treturn StatusReloading\n\tdefault:\n\t\treturn StatusInactive\n\t}\n}\n\n// ParseServiceSubState converts a string sub status to a ServiceSubState enum value\nfunc ParseServiceSubState(subState string) ServiceSubState {\n\tswitch subState {\n\tcase \"dead\":\n\t\treturn SubStateDead\n\tcase \"running\":\n\t\treturn SubStateRunning\n\tcase \"exited\":\n\t\treturn SubStateExited\n\tcase \"failed\":\n\t\treturn SubStateFailed\n\tdefault:\n\t\treturn SubStateUnknown\n\t}\n}\n\n// Service represents a single systemd service with its stats.\ntype Service struct {\n\tName         string          `json:\"n\" cbor:\"0,keyasint\"`\n\tState        ServiceState    `json:\"s\" cbor:\"1,keyasint\"`\n\tCpu          float64         `json:\"c\" cbor:\"2,keyasint\"`\n\tMem          uint64          `json:\"m\" cbor:\"3,keyasint\"`\n\tMemPeak      uint64          `json:\"mp\" cbor:\"4,keyasint\"`\n\tSub          ServiceSubState `json:\"ss\" cbor:\"5,keyasint\"`\n\tCpuPeak      float64         `json:\"cp\" cbor:\"6,keyasint\"`\n\tPrevCpuUsage uint64          `json:\"-\"`\n\tPrevReadTime time.Time       `json:\"-\"`\n}\n\n// UpdateCPUPercent calculates the CPU usage percentage for the service.\nfunc (s *Service) UpdateCPUPercent(cpuUsage uint64) {\n\tnow := time.Now()\n\n\tif s.PrevReadTime.IsZero() || cpuUsage < s.PrevCpuUsage {\n\t\ts.Cpu = 0\n\t\ts.PrevCpuUsage = cpuUsage\n\t\ts.PrevReadTime = now\n\t\treturn\n\t}\n\n\tduration := now.Sub(s.PrevReadTime).Nanoseconds()\n\tif duration <= 0 {\n\t\ts.PrevCpuUsage = cpuUsage\n\t\ts.PrevReadTime = now\n\t\treturn\n\t}\n\n\tcoreCount := int64(runtime.NumCPU())\n\tduration *= coreCount\n\n\tusageDelta := cpuUsage - s.PrevCpuUsage\n\tcpuPercent := float64(usageDelta) / float64(duration)\n\ts.Cpu = twoDecimals(cpuPercent * 100)\n\n\tif s.Cpu > s.CpuPeak {\n\t\ts.CpuPeak = s.Cpu\n\t}\n\n\ts.PrevCpuUsage = cpuUsage\n\ts.PrevReadTime = now\n}\n\nfunc twoDecimals(value float64) float64 {\n\treturn math.Round(value*100) / 100\n}\n\n// ServiceDependency represents a unit that the service depends on.\ntype ServiceDependency struct {\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description,omitempty\"`\n\tActiveState string `json:\"activeState,omitempty\"`\n\tSubState    string `json:\"subState,omitempty\"`\n}\n\n// ServiceDetails contains extended information about a systemd service.\ntype ServiceDetails map[string]any\n"
  },
  {
    "path": "internal/entities/systemd/systemd_test.go",
    "content": "//go:build testing\n\npackage systemd_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseServiceStatus(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected systemd.ServiceState\n\t}{\n\t\t{\"active\", systemd.StatusActive},\n\t\t{\"inactive\", systemd.StatusInactive},\n\t\t{\"failed\", systemd.StatusFailed},\n\t\t{\"activating\", systemd.StatusActivating},\n\t\t{\"deactivating\", systemd.StatusDeactivating},\n\t\t{\"reloading\", systemd.StatusReloading},\n\t\t{\"unknown\", systemd.StatusInactive}, // default case\n\t\t{\"\", systemd.StatusInactive},        // default case\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.input, func(t *testing.T) {\n\t\t\tresult := systemd.ParseServiceStatus(test.input)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestParseServiceSubState(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected systemd.ServiceSubState\n\t}{\n\t\t{\"dead\", systemd.SubStateDead},\n\t\t{\"running\", systemd.SubStateRunning},\n\t\t{\"exited\", systemd.SubStateExited},\n\t\t{\"failed\", systemd.SubStateFailed},\n\t\t{\"unknown\", systemd.SubStateUnknown},\n\t\t{\"other\", systemd.SubStateUnknown}, // default case\n\t\t{\"\", systemd.SubStateUnknown},      // default case\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.input, func(t *testing.T) {\n\t\t\tresult := systemd.ParseServiceSubState(test.input)\n\t\t\tassert.Equal(t, test.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestServiceUpdateCPUPercent(t *testing.T) {\n\tt.Run(\"initial call sets CPU to 0\", func(t *testing.T) {\n\t\tservice := &systemd.Service{}\n\t\tservice.UpdateCPUPercent(1000)\n\t\tassert.Equal(t, 0.0, service.Cpu)\n\t\tassert.Equal(t, uint64(1000), service.PrevCpuUsage)\n\t\tassert.False(t, service.PrevReadTime.IsZero())\n\t})\n\n\tt.Run(\"subsequent call calculates CPU percentage\", func(t *testing.T) {\n\t\tservice := &systemd.Service{}\n\t\tservice.PrevCpuUsage = 1000\n\t\tservice.PrevReadTime = time.Now().Add(-time.Second)\n\n\t\tservice.UpdateCPUPercent(8000000000) // 8 seconds of CPU time\n\n\t\t// CPU usage should be positive and reasonable\n\t\tassert.Greater(t, service.Cpu, 0.0, \"CPU usage should be positive\")\n\t\tassert.LessOrEqual(t, service.Cpu, 100.0, \"CPU usage should not exceed 100%\")\n\t\tassert.Equal(t, uint64(8000000000), service.PrevCpuUsage)\n\t\tassert.Greater(t, service.CpuPeak, 0.0, \"CPU peak should be set\")\n\t})\n\n\tt.Run(\"CPU peak updates only when higher\", func(t *testing.T) {\n\t\tservice := &systemd.Service{}\n\t\tservice.PrevCpuUsage = 1000\n\t\tservice.PrevReadTime = time.Now().Add(-time.Second)\n\t\tservice.UpdateCPUPercent(8000000000) // Set initial peak to ~50%\n\t\tinitialPeak := service.CpuPeak\n\n\t\t// Now try with much lower CPU usage - should not update peak\n\t\tservice.PrevReadTime = time.Now().Add(-time.Second)\n\t\tservice.UpdateCPUPercent(1000000) // Much lower usage\n\t\tassert.Equal(t, initialPeak, service.CpuPeak, \"Peak should not update for lower CPU usage\")\n\t})\n\n\tt.Run(\"handles zero duration\", func(t *testing.T) {\n\t\tservice := &systemd.Service{}\n\t\tservice.PrevCpuUsage = 1000\n\t\tnow := time.Now()\n\t\tservice.PrevReadTime = now\n\t\t// Mock time.Now() to return the same time to ensure zero duration\n\t\t// Since we can't mock time in Go easily, we'll check the logic manually\n\t\t// The zero duration case happens when duration <= 0\n\t\tassert.Equal(t, 0.0, service.Cpu, \"CPU should start at 0\")\n\t})\n\n\tt.Run(\"handles CPU usage wraparound\", func(t *testing.T) {\n\t\tservice := &systemd.Service{}\n\t\t// Simulate wraparound where new usage is less than previous\n\t\tservice.PrevCpuUsage = 1000\n\t\tservice.PrevReadTime = time.Now().Add(-time.Second)\n\t\tservice.UpdateCPUPercent(500) // Less than previous, should reset\n\t\tassert.Equal(t, 0.0, service.Cpu)\n\t})\n}\n"
  },
  {
    "path": "internal/ghupdate/extract.go",
    "content": "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\"\n)\n\n// extract extracts an archive file to the destination directory.\n// Supports .zip and .tar.gz files based on the file extension.\nfunc extract(srcPath, destDir string) error {\n\tif strings.HasSuffix(srcPath, \".tar.gz\") {\n\t\treturn extractTarGz(srcPath, destDir)\n\t}\n\t// Default to zip extraction\n\treturn extractZip(srcPath, destDir)\n}\n\n// extractTarGz extracts a tar.gz archive to the destination directory.\nfunc extractTarGz(srcPath, destDir string) error {\n\tsrc, err := os.Open(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer src.Close()\n\n\tgz, err := gzip.NewReader(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer gz.Close()\n\n\ttr := tar.NewReader(gz)\n\n\tfor {\n\t\theader, err := tr.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif header.Typeflag == tar.TypeDir {\n\t\t\tif err := os.MkdirAll(filepath.Join(destDir, header.Name), 0755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(filepath.Join(destDir, header.Name)), 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\toutFile, err := os.Create(filepath.Join(destDir, header.Name))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif _, err := io.Copy(outFile, tr); err != nil {\n\t\t\toutFile.Close()\n\t\t\treturn err\n\t\t}\n\t\toutFile.Close()\n\t}\n\n\treturn nil\n}\n\n// extractZip extracts the zip archive at \"src\" to \"dest\".\n//\n// Note that only dirs and regular files will be extracted.\n// Symbolic links, named pipes, sockets, or any other irregular files\n// are skipped because they come with too many edge cases and ambiguities.\nfunc extractZip(src, dest string) error {\n\tzr, err := zip.OpenReader(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer zr.Close()\n\n\t// normalize dest path to check later for Zip Slip\n\tdest = filepath.Clean(dest) + string(os.PathSeparator)\n\n\tfor _, f := range zr.File {\n\t\terr := extractFile(f, dest)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// extractFile extracts the provided zipFile into \"basePath/zipFileName\" path,\n// creating all the necessary path directories.\nfunc extractFile(zipFile *zip.File, basePath string) error {\n\tpath := filepath.Join(basePath, zipFile.Name)\n\n\t// check for Zip Slip\n\tif !strings.HasPrefix(path, basePath) {\n\t\treturn fmt.Errorf(\"invalid file path: %s\", path)\n\t}\n\n\tr, err := zipFile.Open()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer r.Close()\n\n\t// allow only dirs or regular files\n\tif zipFile.FileInfo().IsDir() {\n\t\tif err := os.MkdirAll(path, os.ModePerm); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if zipFile.FileInfo().Mode().IsRegular() {\n\t\t// ensure that the file path directories are created\n\t\tif err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer f.Close()\n\n\t\t_, err = io.Copy(f, r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/ghupdate/ghupdate.go",
    "content": "// Package ghupdate implements a new command to self update the current\n// executable with the latest GitHub release. This is based on PocketBase's\n// ghupdate package with modifications.\npackage ghupdate\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/henrygd/beszel\"\n\n\t\"github.com/blang/semver\"\n)\n\n// Minimal color functions using ANSI escape codes\nconst (\n\tcolorReset  = \"\\033[0m\"\n\tColorYellow = \"\\033[33m\"\n\tColorGreen  = \"\\033[32m\"\n\tcolorCyan   = \"\\033[36m\"\n\tcolorGray   = \"\\033[90m\"\n)\n\nfunc ColorPrint(color, text string) {\n\tfmt.Println(color + text + colorReset)\n}\n\nfunc ColorPrintf(color, format string, args ...any) {\n\tfmt.Printf(color+format+colorReset+\"\\n\", args...)\n}\n\n// HttpClient is a base HTTP client interface (usually used for test purposes).\ntype HttpClient interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// Config defines the config options of the ghupdate plugin.\n//\n// NB! This plugin is considered experimental and its config options may change in the future.\ntype Config struct {\n\t// Owner specifies the account owner of the repository (default to \"pocketbase\").\n\tOwner string\n\n\t// Repo specifies the name of the repository (default to \"pocketbase\").\n\tRepo string\n\n\t// ArchiveExecutable specifies the name of the executable file in the release archive\n\t// (default to \"pocketbase\"; an additional \".exe\" check is also performed as a fallback).\n\tArchiveExecutable string\n\n\t// Optional context to use when fetching and downloading the latest release.\n\tContext context.Context\n\n\t// The HTTP client to use when fetching and downloading the latest release.\n\t// Defaults to `http.DefaultClient`.\n\tHttpClient HttpClient\n\n\t// The data directory to use when fetching and downloading the latest release.\n\tDataDir string\n\n\t// UseMirror specifies whether to use the beszel.dev mirror instead of GitHub API.\n\t// When false (default), always uses api.github.com. When true, uses gh.beszel.dev.\n\tUseMirror bool\n}\n\ntype updater struct {\n\tconfig         Config\n\tcurrentVersion string\n}\n\nfunc Update(config Config) (updated bool, err error) {\n\tp := &updater{\n\t\tcurrentVersion: beszel.Version,\n\t\tconfig:         config,\n\t}\n\n\treturn p.update()\n}\n\nfunc (p *updater) update() (updated bool, err error) {\n\tColorPrint(ColorYellow, \"Fetching release information...\")\n\n\tif p.config.DataDir == \"\" {\n\t\tp.config.DataDir = os.TempDir()\n\t}\n\n\tif p.config.Owner == \"\" {\n\t\tp.config.Owner = \"henrygd\"\n\t}\n\n\tif p.config.Repo == \"\" {\n\t\tp.config.Repo = \"beszel\"\n\t}\n\n\tif p.config.Context == nil {\n\t\tp.config.Context = context.Background()\n\t}\n\n\tif p.config.HttpClient == nil {\n\t\tp.config.HttpClient = http.DefaultClient\n\t}\n\n\tvar latest *release\n\tvar useMirror bool\n\n\t// Determine the API endpoint based on UseMirror flag\n\tapiURL := fmt.Sprintf(\"https://api.github.com/repos/%s/%s/releases/latest\", p.config.Owner, p.config.Repo)\n\tif p.config.UseMirror {\n\t\tuseMirror = true\n\t\tapiURL = fmt.Sprintf(\"https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true\", p.config.Owner, p.config.Repo)\n\t\tColorPrint(ColorYellow, \"Using mirror for update.\")\n\t}\n\n\tlatest, err = fetchLatestRelease(\n\t\tp.config.Context,\n\t\tp.config.HttpClient,\n\t\tapiURL,\n\t)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tcurrentVersion := semver.MustParse(strings.TrimPrefix(p.currentVersion, \"v\"))\n\tnewVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, \"v\"))\n\n\tif newVersion.LTE(currentVersion) {\n\t\tColorPrintf(ColorGreen, \"You already have the latest version %s.\", p.currentVersion)\n\t\treturn false, nil\n\t}\n\n\tsuffix := archiveSuffix(p.config.ArchiveExecutable, runtime.GOOS, runtime.GOARCH)\n\tasset, err := latest.findAssetBySuffix(suffix)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treleaseDir := filepath.Join(p.config.DataDir, \".beszel_update\")\n\tdefer os.RemoveAll(releaseDir)\n\n\tColorPrintf(ColorYellow, \"Downloading %s...\", asset.Name)\n\n\t// download the release asset\n\tassetPath := filepath.Join(releaseDir, asset.Name)\n\tif err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, useMirror); err != nil {\n\t\treturn false, err\n\t}\n\n\tColorPrintf(ColorYellow, \"Extracting %s...\", asset.Name)\n\n\textractDir := filepath.Join(releaseDir, \"extracted_\"+asset.Name)\n\tdefer os.RemoveAll(extractDir)\n\n\t// Extract the archive (automatically detects format)\n\tif err := extract(assetPath, extractDir); err != nil {\n\t\treturn false, err\n\t}\n\n\tColorPrint(ColorYellow, \"Replacing the executable...\")\n\n\toldExec, err := os.Executable()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\trenamedOldExec := oldExec + \".old\"\n\tdefer os.Remove(renamedOldExec)\n\n\tnewExec := filepath.Join(extractDir, p.config.ArchiveExecutable)\n\tif _, err := os.Stat(newExec); err != nil {\n\t\t// try again with an .exe extension\n\t\tnewExec = newExec + \".exe\"\n\t\tif _, fallbackErr := os.Stat(newExec); fallbackErr != nil {\n\t\t\treturn false, fmt.Errorf(\"the executable in the extracted path is missing or it is inaccessible: %v, %v\", err, fallbackErr)\n\t\t}\n\t}\n\n\t// rename the current executable\n\tif err := os.Rename(oldExec, renamedOldExec); err != nil {\n\t\treturn false, fmt.Errorf(\"failed to rename the current executable: %w\", err)\n\t}\n\n\ttryToRevertExecChanges := func() {\n\t\tif revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil {\n\t\t\tslog.Debug(\n\t\t\t\t\"Failed to revert executable\",\n\t\t\t\tslog.String(\"old\", renamedOldExec),\n\t\t\t\tslog.String(\"new\", oldExec),\n\t\t\t\tslog.String(\"error\", revertErr.Error()),\n\t\t\t)\n\t\t}\n\t}\n\n\t// replace with the extracted binary\n\tif err := os.Rename(newExec, oldExec); err != nil {\n\t\t// If rename fails due to cross-device link, try copying instead\n\t\tif isCrossDeviceError(err) {\n\t\t\tif err := copyFile(newExec, oldExec); err != nil {\n\t\t\t\ttryToRevertExecChanges()\n\t\t\t\treturn false, fmt.Errorf(\"failed replacing the executable: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\ttryToRevertExecChanges()\n\t\t\treturn false, fmt.Errorf(\"failed replacing the executable: %w\", err)\n\t\t}\n\t}\n\n\tColorPrint(colorGray, \"---\")\n\tColorPrint(ColorGreen, \"Update completed successfully!\")\n\n\t// print the release notes\n\tif latest.Body != \"\" {\n\t\tfmt.Print(\"\\n\")\n\t\treleaseNotes := strings.TrimSpace(strings.Replace(latest.Body, \"> _To update the prebuilt executable you can run `./\"+p.config.ArchiveExecutable+\" update`._\", \"\", 1))\n\t\tColorPrint(colorCyan, releaseNotes)\n\t\tfmt.Print(\"\\n\")\n\t}\n\n\treturn true, nil\n}\n\nfunc fetchLatestRelease(\n\tctx context.Context,\n\tclient HttpClient,\n\turl string,\n) (*release, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\trawBody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// http.Client doesn't treat non 2xx responses as error\n\tif res.StatusCode >= 400 {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"(%d) failed to fetch latest releases:\\n%s\",\n\t\t\tres.StatusCode,\n\t\t\tstring(rawBody),\n\t\t)\n\t}\n\n\tresult := &release{}\n\tif err := json.Unmarshal(rawBody, result); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n\nfunc downloadFile(\n\tctx context.Context,\n\tclient HttpClient,\n\turl string,\n\tdestPath string,\n\tuseMirror bool,\n) error {\n\tif useMirror {\n\t\turl = strings.Replace(url, \"github.com\", \"gh.beszel.dev\", 1)\n\t}\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\t// http.Client doesn't treat non 2xx responses as error\n\tif res.StatusCode >= 400 {\n\t\treturn fmt.Errorf(\"(%d) failed to send download file request\", res.StatusCode)\n\t}\n\n\t// ensure that the dest parent dir(s) exist\n\tif err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\tdest, err := os.Create(destPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dest.Close()\n\n\tif _, err := io.Copy(dest, res.Body); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// isCrossDeviceError checks if the error is due to a cross-device link\nfunc isCrossDeviceError(err error) bool {\n\treturn err != nil && (strings.Contains(err.Error(), \"cross-device\") ||\n\t\tstrings.Contains(err.Error(), \"EXDEV\"))\n}\n\n// copyFile copies a file from src to dst, preserving permissions\nfunc copyFile(src, dst string) error {\n\tsourceFile, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sourceFile.Close()\n\n\tdestFile, err := os.Create(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer destFile.Close()\n\n\t// Copy the file contents\n\tif _, err := io.Copy(destFile, sourceFile); err != nil {\n\t\treturn err\n\t}\n\n\t// Preserve the original file permissions\n\tsourceInfo, err := sourceFile.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn destFile.Chmod(sourceInfo.Mode())\n}\n\nfunc archiveSuffix(binaryName, goos, goarch string) string {\n\tif goos == \"windows\" {\n\t\treturn fmt.Sprintf(\"%s_%s_%s.zip\", binaryName, goos, goarch)\n\t}\n\t// Use glibc build for agent on glibc systems (includes NVML support via purego)\n\tif binaryName == \"beszel-agent\" && goos == \"linux\" && goarch == \"amd64\" && isGlibc() {\n\t\treturn fmt.Sprintf(\"%s_%s_%s_glibc.tar.gz\", binaryName, goos, goarch)\n\t}\n\treturn fmt.Sprintf(\"%s_%s_%s.tar.gz\", binaryName, goos, goarch)\n}\n\nfunc isGlibc() bool {\n\tfor _, path := range []string{\n\t\t\"/lib64/ld-linux-x86-64.so.2\",                // common on many distros\n\t\t\"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2\", // Debian/Ubuntu\n\t\t\"/lib/ld-linux-x86-64.so.2\",                  // alternate\n\t} {\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\t// Fallback to ldd output when present (musl ldd reports musl, glibc reports GNU libc/glibc).\n\tif lddPath, err := exec.LookPath(\"ldd\"); err == nil {\n\t\tout, err := exec.Command(lddPath, \"--version\").CombinedOutput()\n\t\tif err == nil {\n\t\t\ts := strings.ToLower(string(out))\n\t\t\tif strings.Contains(s, \"gnu libc\") || strings.Contains(s, \"glibc\") {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/ghupdate/ghupdate_test.go",
    "content": "package ghupdate\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestReleaseFindAssetBySuffix(t *testing.T) {\n\tr := release{\n\t\tAssets: []*releaseAsset{\n\t\t\t{Name: \"test1.zip\", Id: 1},\n\t\t\t{Name: \"test2.zip\", Id: 2},\n\t\t\t{Name: \"test22.zip\", Id: 22},\n\t\t\t{Name: \"test3.zip\", Id: 3},\n\t\t},\n\t}\n\n\tasset, err := r.findAssetBySuffix(\"2.zip\")\n\tif err != nil {\n\t\tt.Fatalf(\"Expected nil, got err: %v\", err)\n\t}\n\n\tif asset.Id != 2 {\n\t\tt.Fatalf(\"Expected asset with id %d, got %v\", 2, asset)\n\t}\n}\n\nfunc TestExtractFailure(t *testing.T) {\n\ttestDir := t.TempDir()\n\n\t// Test with missing zip file\n\tmissingZipPath := filepath.Join(testDir, \"missing_test.zip\")\n\textractedPath := filepath.Join(testDir, \"zip_extract\")\n\n\tif err := extract(missingZipPath, extractedPath); err == nil {\n\t\tt.Fatal(\"Expected Extract to fail due to missing zip file\")\n\t}\n\n\t// Test with missing tar.gz file\n\tmissingTarPath := filepath.Join(testDir, \"missing_test.tar.gz\")\n\n\tif err := extract(missingTarPath, extractedPath); err == nil {\n\t\tt.Fatal(\"Expected Extract to fail due to missing tar.gz file\")\n\t}\n}\n"
  },
  {
    "path": "internal/ghupdate/release.go",
    "content": "package ghupdate\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\ntype releaseAsset struct {\n\tName        string `json:\"name\"`\n\tDownloadUrl string `json:\"browser_download_url\"`\n\tId          int    `json:\"id\"`\n\tSize        int    `json:\"size\"`\n}\n\ntype release struct {\n\tName      string          `json:\"name\"`\n\tTag       string          `json:\"tag_name\"`\n\tPublished string          `json:\"published_at\"`\n\tUrl       string          `json:\"html_url\"`\n\tBody      string          `json:\"body\"`\n\tAssets    []*releaseAsset `json:\"assets\"`\n\tId        int             `json:\"id\"`\n}\n\n// findAssetBySuffix returns the first available asset containing the specified suffix.\nfunc (r *release) findAssetBySuffix(suffix string) (*releaseAsset, error) {\n\tif suffix != \"\" {\n\t\tfor _, asset := range r.Assets {\n\t\t\tif strings.HasSuffix(asset.Name, suffix) {\n\t\t\t\treturn asset, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"missing asset containing \" + suffix)\n}\n"
  },
  {
    "path": "internal/ghupdate/selinux.go",
    "content": "package ghupdate\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// HandleSELinuxContext restores or applies the correct SELinux label to the binary.\nfunc HandleSELinuxContext(path string) error {\n\tout, err := exec.Command(\"getenforce\").Output()\n\tif err != nil {\n\t\t// SELinux not enabled or getenforce not available\n\t\treturn nil\n\t}\n\tstate := strings.TrimSpace(string(out))\n\tif state == \"Disabled\" {\n\t\treturn nil\n\t}\n\n\tColorPrint(ColorYellow, \"SELinux is enabled; applying context…\")\n\n\t// Try persistent context via semanage+restorecon\n\tif success := trySemanageRestorecon(path); success {\n\t\treturn nil\n\t}\n\n\t// Fallback to temporary context via chcon\n\tif chconPath, err := exec.LookPath(\"chcon\"); err == nil {\n\t\tif err := exec.Command(chconPath, \"-t\", \"bin_t\", path).Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"chcon failed: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"no SELinux tools available (semanage/restorecon or chcon)\")\n}\n\n// trySemanageRestorecon attempts to set persistent SELinux context using semanage and restorecon.\n// Returns true if successful, false otherwise.\nfunc trySemanageRestorecon(path string) bool {\n\tsemanagePath, err := exec.LookPath(\"semanage\")\n\tif err != nil {\n\t\treturn false\n\t}\n\n\trestoreconPath, err := exec.LookPath(\"restorecon\")\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// Try to add the fcontext rule; if it already exists, try to modify it\n\tif err := exec.Command(semanagePath, \"fcontext\", \"-a\", \"-t\", \"bin_t\", path).Run(); err != nil {\n\t\t// Rule may already exist, try modify instead\n\t\tif err := exec.Command(semanagePath, \"fcontext\", \"-m\", \"-t\", \"bin_t\", path).Run(); err != nil {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Apply the context with restorecon\n\tif err := exec.Command(restoreconPath, \"-v\", path).Run(); err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "internal/ghupdate/selinux_test.go",
    "content": "package ghupdate\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestHandleSELinuxContext_NoSELinux(t *testing.T) {\n\t// Skip on SELinux systems - this test is for non-SELinux behavior\n\tif _, err := exec.LookPath(\"getenforce\"); err == nil {\n\t\tt.Skip(\"skipping on SELinux-enabled system\")\n\t}\n\n\t// On systems without SELinux, getenforce will fail and the function\n\t// should return nil without error\n\ttempFile := filepath.Join(t.TempDir(), \"test-binary\")\n\tif err := os.WriteFile(tempFile, []byte(\"test\"), 0755); err != nil {\n\t\tt.Fatalf(\"failed to create temp file: %v\", err)\n\t}\n\n\terr := HandleSELinuxContext(tempFile)\n\tif err != nil {\n\t\tt.Errorf(\"HandleSELinuxContext() on non-SELinux system returned error: %v\", err)\n\t}\n}\n\nfunc TestHandleSELinuxContext_InvalidPath(t *testing.T) {\n\t// Skip on SELinux systems - this test is for non-SELinux behavior\n\tif _, err := exec.LookPath(\"getenforce\"); err == nil {\n\t\tt.Skip(\"skipping on SELinux-enabled system\")\n\t}\n\n\t// On non-SELinux systems, getenforce fails early so even invalid paths succeed\n\terr := HandleSELinuxContext(\"/nonexistent/path/binary\")\n\tif err != nil {\n\t\tt.Errorf(\"HandleSELinuxContext() with invalid path on non-SELinux system returned error: %v\", err)\n\t}\n}\n\nfunc TestTrySemanageRestorecon_NoTools(t *testing.T) {\n\t// Skip if semanage is available (we don't want to modify system SELinux policy)\n\tif _, err := exec.LookPath(\"semanage\"); err == nil {\n\t\tt.Skip(\"skipping on system with semanage available\")\n\t}\n\n\t// Should return false when semanage is not available\n\tresult := trySemanageRestorecon(\"/some/path\")\n\tif result {\n\t\tt.Error(\"trySemanageRestorecon() returned true when semanage is not available\")\n\t}\n}\n"
  },
  {
    "path": "internal/hub/agent_connect.go",
    "content": "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/internal/common\"\n\t\"github.com/henrygd/beszel/internal/hub/expirymap\"\n\t\"github.com/henrygd/beszel/internal/hub/ws\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/lxzan/gws\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// agentConnectRequest holds information related to an agent's connection attempt.\ntype agentConnectRequest struct {\n\thub         *Hub\n\treq         *http.Request\n\tres         http.ResponseWriter\n\ttoken       string\n\tagentSemVer semver.Version\n\t// isUniversalToken is true if the token is a universal token.\n\tisUniversalToken bool\n\t// userId is the user ID associated with the universal token.\n\tuserId string\n}\n\n// universalTokenMap stores active universal tokens and their associated user IDs.\nvar universalTokenMap tokenMap\n\ntype tokenMap struct {\n\tstore *expirymap.ExpiryMap[string]\n\tonce  sync.Once\n}\n\n// getMap returns the expirymap, creating it if necessary.\nfunc (tm *tokenMap) GetMap() *expirymap.ExpiryMap[string] {\n\ttm.once.Do(func() {\n\t\ttm.store = expirymap.New[string](time.Hour)\n\t})\n\treturn tm.store\n}\n\n// handleAgentConnect is the HTTP handler for an agent's connection request.\nfunc (h *Hub) handleAgentConnect(e *core.RequestEvent) error {\n\tagentRequest := agentConnectRequest{req: e.Request, res: e.Response, hub: h}\n\t_ = agentRequest.agentConnect()\n\treturn nil\n}\n\n// agentConnect validates agent credentials and upgrades the connection to a WebSocket.\nfunc (acr *agentConnectRequest) agentConnect() (err error) {\n\tvar agentVersion string\n\n\tacr.token, agentVersion, err = acr.validateAgentHeaders(acr.req.Header)\n\tif err != nil {\n\t\treturn acr.sendResponseError(acr.res, http.StatusBadRequest, \"\")\n\t}\n\n\t// Check if token is an active universal token\n\tacr.userId, acr.isUniversalToken = universalTokenMap.GetMap().GetOk(acr.token)\n\tif !acr.isUniversalToken {\n\t\t// Fallback: check for a permanent universal token stored in the DB\n\t\tif rec, err := acr.hub.FindFirstRecordByFilter(\"universal_tokens\", \"token = {:token}\", dbx.Params{\"token\": acr.token}); err == nil {\n\t\t\tif userID := rec.GetString(\"user\"); userID != \"\" {\n\t\t\t\tacr.userId = userID\n\t\t\t\tacr.isUniversalToken = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find matching fingerprint records for this token\n\tfpRecords := getFingerprintRecordsByToken(acr.token, acr.hub)\n\tif len(fpRecords) == 0 && !acr.isUniversalToken {\n\t\t// Invalid token - no records found and not a universal token\n\t\treturn acr.sendResponseError(acr.res, http.StatusUnauthorized, \"Invalid token\")\n\t}\n\n\t// Validate agent version\n\tacr.agentSemVer, err = semver.Parse(agentVersion)\n\tif err != nil {\n\t\treturn acr.sendResponseError(acr.res, http.StatusUnauthorized, \"Invalid agent version\")\n\t}\n\n\t// Upgrade connection to WebSocket\n\tconn, err := ws.GetUpgrader().Upgrade(acr.res, acr.req)\n\tif err != nil {\n\t\treturn acr.sendResponseError(acr.res, http.StatusInternalServerError, \"WebSocket upgrade failed\")\n\t}\n\n\tgo acr.verifyWsConn(conn, fpRecords)\n\n\treturn nil\n}\n\n// verifyWsConn verifies the WebSocket connection using the agent's fingerprint and\n// SSH key signature, then adds the system to the system manager.\nfunc (acr *agentConnectRequest) verifyWsConn(conn *gws.Conn, fpRecords []ws.FingerprintRecord) (err error) {\n\twsConn := ws.NewWsConnection(conn, acr.agentSemVer)\n\n\t// must set wsConn in connection store before the read loop\n\tconn.Session().Store(\"wsConn\", wsConn)\n\n\t// make sure connection is closed if there is an error\n\tdefer func() {\n\t\tif err != nil {\n\t\t\twsConn.Close([]byte(err.Error()))\n\t\t}\n\t}()\n\n\tgo conn.ReadLoop()\n\n\tsigner, err := acr.hub.GetSSHKey(\"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tagentFingerprint, err := wsConn.GetFingerprint(context.Background(), acr.token, signer, acr.isUniversalToken)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Find or create the appropriate system for this token and fingerprint\n\tfpRecord, err := acr.findOrCreateSystemForToken(fpRecords, agentFingerprint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn acr.hub.sm.AddWebSocketSystem(fpRecord.SystemId, acr.agentSemVer, wsConn)\n}\n\n// validateAgentHeaders extracts and validates the token and agent version from HTTP headers.\nfunc (acr *agentConnectRequest) validateAgentHeaders(headers http.Header) (string, string, error) {\n\ttoken := headers.Get(\"X-Token\")\n\tagentVersion := headers.Get(\"X-Beszel\")\n\n\tif agentVersion == \"\" || token == \"\" || len(token) > 64 {\n\t\treturn \"\", \"\", errors.New(\"\")\n\t}\n\treturn token, agentVersion, nil\n}\n\n// sendResponseError writes an HTTP error response.\nfunc (acr *agentConnectRequest) sendResponseError(res http.ResponseWriter, code int, message string) error {\n\tres.WriteHeader(code)\n\tif message != \"\" {\n\t\tres.Write([]byte(message))\n\t}\n\treturn nil\n}\n\n// getFingerprintRecordsByToken retrieves all fingerprint records associated with a given token.\nfunc getFingerprintRecordsByToken(token string, h *Hub) []ws.FingerprintRecord {\n\tvar records []ws.FingerprintRecord\n\t// All will populate empty slice even on error\n\t_ = h.DB().NewQuery(\"SELECT id, system, fingerprint, token FROM fingerprints WHERE token = {:token}\").\n\t\tBind(dbx.Params{\n\t\t\t\"token\": token,\n\t\t}).\n\t\tAll(&records)\n\treturn records\n}\n\n// findOrCreateSystemForToken finds an existing system matching the token and fingerprint,\n// or creates a new one for a universal token.\nfunc (acr *agentConnectRequest) findOrCreateSystemForToken(fpRecords []ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {\n\t// No records - only valid for active universal tokens\n\tif len(fpRecords) == 0 {\n\t\treturn acr.handleNoRecords(agentFingerprint)\n\t}\n\n\t// Single record - handle as regular token\n\tif len(fpRecords) == 1 && !acr.isUniversalToken {\n\t\treturn acr.handleSingleRecord(fpRecords[0], agentFingerprint)\n\t}\n\n\t// Multiple records or universal token - look for matching fingerprint\n\treturn acr.handleMultipleRecordsOrUniversalToken(fpRecords, agentFingerprint)\n}\n\n// handleNoRecords handles the case where no fingerprint records are found for a token.\n// A new system is created if the token is a valid universal token.\nfunc (acr *agentConnectRequest) handleNoRecords(agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {\n\tvar fpRecord ws.FingerprintRecord\n\n\tif !acr.isUniversalToken || acr.userId == \"\" {\n\t\treturn fpRecord, errors.New(\"no matching fingerprints\")\n\t}\n\n\treturn acr.createNewSystemForUniversalToken(agentFingerprint)\n}\n\n// handleSingleRecord handles the case with a single fingerprint record. It validates\n// the agent's fingerprint against the stored one, or sets it on first connect.\nfunc (acr *agentConnectRequest) handleSingleRecord(fpRecord ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {\n\t// If no current fingerprint, update with new fingerprint (first time connecting)\n\tif fpRecord.Fingerprint == \"\" {\n\t\tif err := acr.hub.SetFingerprint(&fpRecord, agentFingerprint.Fingerprint); err != nil {\n\t\t\treturn fpRecord, err\n\t\t}\n\t\t// Update the record with the fingerprint that was set\n\t\tfpRecord.Fingerprint = agentFingerprint.Fingerprint\n\t\treturn fpRecord, nil\n\t}\n\n\t// Abort if fingerprint exists but doesn't match (different machine)\n\tif fpRecord.Fingerprint != agentFingerprint.Fingerprint {\n\t\treturn fpRecord, errors.New(\"fingerprint mismatch\")\n\t}\n\n\treturn fpRecord, nil\n}\n\n// handleMultipleRecordsOrUniversalToken finds a matching fingerprint from multiple records.\n// If no match is found and the token is a universal token, a new system is created.\nfunc (acr *agentConnectRequest) handleMultipleRecordsOrUniversalToken(fpRecords []ws.FingerprintRecord, agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {\n\t// Return existing record with matching fingerprint if found\n\tfor i := range fpRecords {\n\t\tif fpRecords[i].Fingerprint == agentFingerprint.Fingerprint {\n\t\t\treturn fpRecords[i], nil\n\t\t}\n\t}\n\n\t// No matching fingerprint record found, but it's\n\t// an active universal token so create a new system\n\tif acr.isUniversalToken {\n\t\treturn acr.createNewSystemForUniversalToken(agentFingerprint)\n\t}\n\n\treturn ws.FingerprintRecord{}, errors.New(\"fingerprint mismatch\")\n}\n\n// createNewSystemForUniversalToken creates a new system and fingerprint record for a universal token.\nfunc (acr *agentConnectRequest) createNewSystemForUniversalToken(agentFingerprint common.FingerprintResponse) (ws.FingerprintRecord, error) {\n\tvar fpRecord ws.FingerprintRecord\n\tif !acr.isUniversalToken || acr.userId == \"\" {\n\t\treturn fpRecord, errors.New(\"invalid token\")\n\t}\n\n\tfpRecord.Token = acr.token\n\n\tsystemId, err := acr.createSystem(agentFingerprint)\n\tif err != nil {\n\t\treturn fpRecord, err\n\t}\n\tfpRecord.SystemId = systemId\n\n\t// Set the fingerprint for the new system\n\tif err := acr.hub.SetFingerprint(&fpRecord, agentFingerprint.Fingerprint); err != nil {\n\t\treturn fpRecord, err\n\t}\n\n\t// Update the record with the fingerprint that was set\n\tfpRecord.Fingerprint = agentFingerprint.Fingerprint\n\n\treturn fpRecord, nil\n}\n\n// createSystem creates a new system record in the database using details from the agent.\nfunc (acr *agentConnectRequest) createSystem(agentFingerprint common.FingerprintResponse) (recordId string, err error) {\n\tsystemsCollection, err := acr.hub.FindCachedCollectionByNameOrId(\"systems\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tremoteAddr := getRealIP(acr.req)\n\t// separate port from address\n\tif agentFingerprint.Hostname == \"\" {\n\t\tagentFingerprint.Hostname = remoteAddr\n\t}\n\tif agentFingerprint.Port == \"\" {\n\t\tagentFingerprint.Port = \"45876\"\n\t}\n\tif agentFingerprint.Name == \"\" {\n\t\tagentFingerprint.Name = agentFingerprint.Hostname\n\t}\n\t// create new record\n\tsystemRecord := core.NewRecord(systemsCollection)\n\tsystemRecord.Set(\"name\", agentFingerprint.Name)\n\tsystemRecord.Set(\"host\", remoteAddr)\n\tsystemRecord.Set(\"port\", agentFingerprint.Port)\n\tsystemRecord.Set(\"users\", []string{acr.userId})\n\n\treturn systemRecord.Id, acr.hub.Save(systemRecord)\n}\n\n// SetFingerprint creates or updates a fingerprint record in the database.\nfunc (h *Hub) SetFingerprint(fpRecord *ws.FingerprintRecord, fingerprint string) (err error) {\n\t// // can't use raw query here because it doesn't trigger SSE\n\tvar record *core.Record\n\tswitch fpRecord.Id {\n\tcase \"\":\n\t\t// create new record for universal token\n\t\tcollection, _ := h.FindCachedCollectionByNameOrId(\"fingerprints\")\n\t\trecord = core.NewRecord(collection)\n\t\trecord.Set(\"system\", fpRecord.SystemId)\n\tdefault:\n\t\trecord, err = h.FindRecordById(\"fingerprints\", fpRecord.Id)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\trecord.Set(\"token\", fpRecord.Token)\n\trecord.Set(\"fingerprint\", fingerprint)\n\treturn h.SaveNoValidate(record)\n}\n\n// getRealIP extracts the client's real IP address from request headers,\n// checking common proxy headers before falling back to the remote address.\nfunc getRealIP(r *http.Request) string {\n\tif ip := r.Header.Get(\"CF-Connecting-IP\"); ip != \"\" {\n\t\treturn ip\n\t}\n\tif ip := r.Header.Get(\"X-Forwarded-For\"); ip != \"\" {\n\t\t// X-Forwarded-For can contain a comma-separated list: \"client_ip, proxy1, proxy2\"\n\t\t// Take the first one\n\t\tips := strings.Split(ip, \",\")\n\t\tif len(ips) > 0 {\n\t\t\treturn strings.TrimSpace(ips[0])\n\t\t}\n\t}\n\t// Fallback to RemoteAddr\n\tip, _, err := net.SplitHostPort(r.RemoteAddr)\n\tif err != nil {\n\t\treturn r.RemoteAddr\n\t}\n\treturn ip\n}\n"
  },
  {
    "path": "internal/hub/agent_connect_test.go",
    "content": "//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/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/agent\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/hub/ws\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tpbtests \"github.com/pocketbase/pocketbase/tests\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Helper function to create a test hub without import cycle\nfunc createTestHub(t testing.TB) (*Hub, *pbtests.TestApp, error) {\n\ttestDataDir := t.TempDir()\n\ttestApp, err := pbtests.NewTestApp(testDataDir)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn NewHub(testApp), testApp, nil\n}\n\n// cleanupTestHub stops background system goroutines before tearing down the app.\nfunc cleanupTestHub(hub *Hub, testApp *pbtests.TestApp) {\n\tif hub != nil {\n\t\tsm := hub.GetSystemManager()\n\t\tsm.RemoveAllSystems()\n\t\t// Give updater goroutines a brief window to observe cancellation before DB teardown.\n\t\tfor range 20 {\n\t\t\tif sm.GetSystemCount() == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\truntime.Gosched()\n\t\t\ttime.Sleep(5 * time.Millisecond)\n\t\t}\n\t\ttime.Sleep(20 * time.Millisecond)\n\t}\n\tif testApp != nil {\n\t\ttestApp.Cleanup()\n\t}\n}\n\n// Helper function to create a test record\nfunc createTestRecord(app core.App, collection string, data map[string]any) (*core.Record, error) {\n\tcol, err := app.FindCachedCollectionByNameOrId(collection)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trecord := core.NewRecord(col)\n\tfor key, value := range data {\n\t\trecord.Set(key, value)\n\t}\n\n\treturn record, app.Save(record)\n}\n\n// Helper function to create a test user\nfunc createTestUser(app core.App) (*core.Record, error) {\n\tuserRecord, err := createTestRecord(app, \"users\", map[string]any{\n\t\t\"email\":    \"test@test.com\",\n\t\t\"password\": \"testtesttest\",\n\t})\n\treturn userRecord, err\n}\n\n// TestValidateAgentHeaders tests the validateAgentHeaders function\nfunc TestValidateAgentHeaders(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\ttestCases := []struct {\n\t\tname          string\n\t\theaders       http.Header\n\t\texpectError   bool\n\t\texpectedToken string\n\t\texpectedAgent string\n\t}{\n\t\t{\n\t\t\tname: \"valid headers\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Token\":  []string{\"valid-token-123\"},\n\t\t\t\t\"X-Beszel\": []string{\"0.5.0\"},\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedToken: \"valid-token-123\",\n\t\t\texpectedAgent: \"0.5.0\",\n\t\t},\n\t\t{\n\t\t\tname: \"missing token\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Beszel\": []string{\"0.5.0\"},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"missing agent version\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Token\": []string{\"valid-token-123\"},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty token\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Token\":  []string{\"\"},\n\t\t\t\t\"X-Beszel\": []string{\"0.5.0\"},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty agent version\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Token\":  []string{\"valid-token-123\"},\n\t\t\t\t\"X-Beszel\": []string{\"\"},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"token too long\",\n\t\t\theaders: http.Header{\n\t\t\t\t\"X-Token\":  []string{strings.Repeat(\"a\", 65)},\n\t\t\t\t\"X-Beszel\": []string{\"0.5.0\"},\n\t\t\t},\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tacr := &agentConnectRequest{hub: hub}\n\t\t\ttoken, agentVersion, err := acr.validateAgentHeaders(tc.headers)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectedToken, token)\n\t\t\t\tassert.Equal(t, tc.expectedAgent, agentVersion)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestGetAllFingerprintRecordsByToken tests the getAllFingerprintRecordsByToken function\nfunc TestGetAllFingerprintRecordsByToken(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create test data\n\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"pending\",\n\t\t\"users\":  []string{userRecord.Id},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfingerprintRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\"system\":      systemRecord.Id,\n\t\t\"token\":       \"test-token-123\",\n\t\t\"fingerprint\": \"test-fingerprint\",\n\t})\n\tfor i := range 3 {\n\t\tsystemRecord, _ := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\"name\":   fmt.Sprintf(\"test-system-%d\", i),\n\t\t\t\"host\":   \"localhost\",\n\t\t\t\"port\":   \"45876\",\n\t\t\t\"status\": \"pending\",\n\t\t\t\"users\":  []string{userRecord.Id},\n\t\t})\n\t\tcreateTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\"token\":       \"duplicate-token\",\n\t\t\t\"fingerprint\": fmt.Sprintf(\"test-fingerprint-%d\", i),\n\t\t})\n\t}\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tname       string\n\t\ttoken      string\n\t\texpectedId string\n\t\texpectLen  int\n\t}{\n\t\t{\n\t\t\tname:       \"valid token\",\n\t\t\ttoken:      \"test-token-123\",\n\t\t\texpectLen:  1,\n\t\t\texpectedId: fingerprintRecord.Id,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid token\",\n\t\t\ttoken:     \"invalid-token\",\n\t\t\texpectLen: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty token\",\n\t\t\ttoken:     \"\",\n\t\t\texpectLen: 0,\n\t\t},\n\t\t{\n\t\t\tname:      \"duplicate token\",\n\t\t\ttoken:     \"duplicate-token\",\n\t\t\texpectLen: 3,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trecords := getFingerprintRecordsByToken(tc.token, hub)\n\n\t\t\trequire.Len(t, records, tc.expectLen)\n\t\t\tif tc.expectedId != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectedId, records[0].Id)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSetFingerprint tests the SetFingerprint function\nfunc TestSetFingerprint(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create test system\n\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"pending\",\n\t\t\"users\":  []string{userRecord.Id},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create fingerprint record\n\tfingerprintRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\"system\":      systemRecord.Id,\n\t\t\"token\":       \"test-token-123\",\n\t\t\"fingerprint\": \"\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tname           string\n\t\trecordId       string\n\t\tnewFingerprint string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"successful fingerprint update\",\n\t\t\trecordId:       fingerprintRecord.Id,\n\t\t\tnewFingerprint: \"new-test-fingerprint\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"empty fingerprint\",\n\t\t\trecordId:       fingerprintRecord.Id,\n\t\t\tnewFingerprint: \"\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"invalid record ID\",\n\t\t\trecordId:       \"invalid-id\",\n\t\t\tnewFingerprint: \"fingerprint\",\n\t\t\texpectError:    true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := hub.SetFingerprint(&ws.FingerprintRecord{Id: tc.recordId, Token: \"test-token-123\"}, tc.newFingerprint)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify fingerprint was updated\n\t\t\t\tupdatedRecord, err := testApp.FindRecordById(\"fingerprints\", tc.recordId)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.newFingerprint, updatedRecord.GetString(\"fingerprint\"))\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestCreateSystemFromAgentData tests the createSystemFromAgentData function\nfunc TestCreateSystemFromAgentData(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tagentConnReq  agentConnectRequest\n\t\tfingerprint   common.FingerprintResponse\n\t\texpectError   bool\n\t\texpectedName  string\n\t\texpectedHost  string\n\t\texpectedPort  string\n\t\texpectedUsers []string\n\t}{\n\t\t{\n\t\t\tname: \"successful system creation with all fields\",\n\t\t\tagentConnReq: agentConnectRequest{\n\t\t\t\thub:    hub,\n\t\t\t\tuserId: userRecord.Id,\n\t\t\t\treq: &http.Request{\n\t\t\t\t\tRemoteAddr: \"192.168.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tfingerprint: common.FingerprintResponse{\n\t\t\t\tHostname: \"test-server\",\n\t\t\t\tPort:     \"8080\",\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedName:  \"test-server\",\n\t\t\texpectedHost:  \"192.168.0.1\", // This will be the parsed IP from the mock request\n\t\t\texpectedPort:  \"8080\",\n\t\t\texpectedUsers: []string{userRecord.Id},\n\t\t},\n\t\t{\n\t\t\tname: \"system creation with default port\",\n\t\t\tagentConnReq: agentConnectRequest{\n\t\t\t\thub:    hub,\n\t\t\t\tuserId: userRecord.Id,\n\t\t\t\treq: &http.Request{\n\t\t\t\t\tRemoteAddr: \"192.168.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tfingerprint: common.FingerprintResponse{\n\t\t\t\tHostname: \"default-port-server\",\n\t\t\t\tPort:     \"\", // Empty port should default to 45876\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedName:  \"default-port-server\",\n\t\t\texpectedHost:  \"192.168.0.1\", // This will be the parsed IP from the mock request\n\t\t\texpectedPort:  \"45876\",\n\t\t\texpectedUsers: []string{userRecord.Id},\n\t\t},\n\t\t{\n\t\t\tname: \"system creation with empty hostname\",\n\t\t\tagentConnReq: agentConnectRequest{\n\t\t\t\thub:    hub,\n\t\t\t\tuserId: userRecord.Id,\n\t\t\t\treq: &http.Request{\n\t\t\t\t\tRemoteAddr: \"192.168.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tfingerprint: common.FingerprintResponse{\n\t\t\t\tHostname: \"\",\n\t\t\t\tPort:     \"9090\",\n\t\t\t},\n\t\t\texpectError:   false,\n\t\t\texpectedName:  \"192.168.0.1\", // Should fall back to host IP when hostname is empty\n\t\t\texpectedHost:  \"192.168.0.1\", // This will be the parsed IP from the mock request\n\t\t\texpectedPort:  \"9090\",\n\t\t\texpectedUsers: []string{userRecord.Id},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trecordId, err := tc.agentConnReq.createSystem(tc.fingerprint)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotEmpty(t, recordId, \"Record ID should not be empty\")\n\n\t\t\t// Verify the created system record\n\t\t\tsystemRecord, err := testApp.FindRecordById(\"systems\", recordId)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedName, systemRecord.GetString(\"name\"))\n\t\t\tassert.Equal(t, tc.expectedHost, systemRecord.GetString(\"host\"))\n\t\t\tassert.Equal(t, tc.expectedPort, systemRecord.GetString(\"port\"))\n\n\t\t\t// Verify users array\n\t\t\tusers := systemRecord.Get(\"users\")\n\t\t\tassert.Equal(t, tc.expectedUsers, users)\n\t\t})\n\t}\n}\n\n// TestUniversalTokenFlow tests the complete universal token authentication flow\nfunc TestUniversalTokenFlow(t *testing.T) {\n\t_, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(nil, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Set up universal token in the token map\n\tuniversalToken := \"universal-token-123\"\n\n\tuniversalTokenMap.GetMap().Set(universalToken, userRecord.Id, time.Hour)\n\n\ttestCases := []struct {\n\t\tname                string\n\t\ttoken               string\n\t\texpectUniversalAuth bool\n\t\texpectError         bool\n\t\tdescription         string\n\t}{\n\t\t{\n\t\t\tname:                \"valid universal token\",\n\t\t\ttoken:               universalToken,\n\t\t\texpectUniversalAuth: true,\n\t\t\texpectError:         false,\n\t\t\tdescription:         \"Should recognize valid universal token\",\n\t\t},\n\t\t{\n\t\t\tname:                \"invalid universal token\",\n\t\t\ttoken:               \"invalid-universal-token\",\n\t\t\texpectUniversalAuth: false,\n\t\t\texpectError:         true,\n\t\t\tdescription:         \"Should reject invalid universal token\",\n\t\t},\n\t\t{\n\t\t\tname:                \"empty token\",\n\t\t\ttoken:               \"\",\n\t\t\texpectUniversalAuth: false,\n\t\t\texpectError:         true,\n\t\t\tdescription:         \"Should reject empty token\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tacr := &agentConnectRequest{}\n\n\t\t\tacr.userId, acr.isUniversalToken = universalTokenMap.GetMap().GetOk(tc.token)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.False(t, acr.isUniversalToken)\n\t\t\t\tassert.Empty(t, acr.userId)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expectUniversalAuth, acr.isUniversalToken)\n\t\t\t\tif tc.expectUniversalAuth {\n\t\t\t\t\tassert.Equal(t, userRecord.Id, acr.userId)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestAgentConnect tests the agentConnect function with various scenarios\nfunc TestAgentConnect(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create test system\n\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"pending\",\n\t\t\"users\":  []string{userRecord.Id},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create fingerprint record\n\ttestToken := \"test-token-456\"\n\t_, err = createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\"system\":      systemRecord.Id,\n\t\t\"token\":       testToken,\n\t\t\"fingerprint\": \"\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tname           string\n\t\theaders        map[string]string\n\t\texpectedStatus int\n\t\tdescription    string\n\t\terrorMessage   string\n\t}{\n\t\t{\n\t\t\tname: \"missing token header\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Beszel\": \"0.5.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusBadRequest,\n\t\t\tdescription:    \"Should fail due to missing token\",\n\t\t\terrorMessage:   \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"missing agent version header\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\": testToken,\n\t\t\t},\n\t\t\texpectedStatus: http.StatusBadRequest,\n\t\t\tdescription:    \"Should fail due to missing agent version\",\n\t\t\terrorMessage:   \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid token\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\":  \"invalid-token\",\n\t\t\t\t\"X-Beszel\": \"0.5.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusUnauthorized,\n\t\t\tdescription:    \"Should fail due to invalid token\",\n\t\t\terrorMessage:   \"Invalid token\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid agent version\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\":  testToken,\n\t\t\t\t\"X-Beszel\": \"0.5.0.0.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusUnauthorized,\n\t\t\tdescription:    \"Should fail due to invalid agent version\",\n\t\t\terrorMessage:   \"Invalid agent version\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid headers but websocket upgrade will fail in test\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\":  testToken,\n\t\t\t\t\"X-Beszel\": \"0.5.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusInternalServerError,\n\t\t\tdescription:    \"Should pass validation but fail at WebSocket upgrade due to test limitations\",\n\t\t\terrorMessage:   \"WebSocket upgrade failed\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Token too long\",\n\t\t\theaders:        map[string]string{\"X-Token\": strings.Repeat(\"a\", 65), \"X-Beszel\": \"0.5.0\"},\n\t\t\texpectedStatus: http.StatusBadRequest,\n\t\t\tdescription:    \"Should reject token exceeding 64 characters\",\n\t\t\terrorMessage:   \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/api/beszel/agent-connect\", nil)\n\t\t\tfor key, value := range tc.headers {\n\t\t\t\treq.Header.Set(key, value)\n\t\t\t}\n\n\t\t\trecorder := httptest.NewRecorder()\n\t\t\tacr := &agentConnectRequest{\n\t\t\t\thub: hub,\n\t\t\t\treq: req,\n\t\t\t\tres: recorder,\n\t\t\t}\n\t\t\terr = acr.agentConnect()\n\n\t\t\tassert.Equal(t, tc.expectedStatus, recorder.Code, tc.description)\n\t\t\tassert.Equal(t, tc.errorMessage, recorder.Body.String(), tc.description)\n\t\t})\n\t}\n}\n\n// TestSendResponseError tests the sendResponseError function\nfunc TestSendResponseError(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tstatusCode     int\n\t\tmessage        string\n\t\texpectedStatus int\n\t\texpectedBody   string\n\t}{\n\t\t{\n\t\t\tname:           \"unauthorized error\",\n\t\t\tstatusCode:     http.StatusUnauthorized,\n\t\t\tmessage:        \"Invalid token\",\n\t\t\texpectedStatus: http.StatusUnauthorized,\n\t\t\texpectedBody:   \"Invalid token\",\n\t\t},\n\t\t{\n\t\t\tname:           \"bad request error\",\n\t\t\tstatusCode:     http.StatusBadRequest,\n\t\t\tmessage:        \"Missing required header\",\n\t\t\texpectedStatus: http.StatusBadRequest,\n\t\t\texpectedBody:   \"Missing required header\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trecorder := httptest.NewRecorder()\n\t\t\tacr := &agentConnectRequest{}\n\t\t\tacr.sendResponseError(recorder, tc.statusCode, tc.message)\n\n\t\t\tassert.Equal(t, tc.expectedStatus, recorder.Code)\n\t\t\tassert.Equal(t, tc.expectedBody, recorder.Body.String())\n\t\t})\n\t}\n}\n\n// TestHandleAgentConnect tests the HTTP handler\nfunc TestHandleAgentConnect(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create test system\n\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"pending\",\n\t\t\"users\":  []string{userRecord.Id},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create fingerprint record\n\ttestToken := \"test-token-789\"\n\t_, err = createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\"system\":      systemRecord.Id,\n\t\t\"token\":       testToken,\n\t\t\"fingerprint\": \"\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tmethod         string\n\t\theaders        map[string]string\n\t\texpectedStatus int\n\t\tdescription    string\n\t}{\n\t\t{\n\t\t\tname:   \"GET with invalid token\",\n\t\t\tmethod: \"GET\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\":  \"invalid\",\n\t\t\t\t\"X-Beszel\": \"0.5.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusUnauthorized,\n\t\t\tdescription:    \"Should reject invalid token\",\n\t\t},\n\t\t{\n\t\t\tname:   \"GET with valid token\",\n\t\t\tmethod: \"GET\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Token\":  testToken,\n\t\t\t\t\"X-Beszel\": \"0.5.0\",\n\t\t\t},\n\t\t\texpectedStatus: http.StatusInternalServerError, // WebSocket upgrade fails in test\n\t\t\tdescription:    \"Should pass validation but fail at WebSocket upgrade\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(tc.method, \"/api/beszel/agent-connect\", nil)\n\t\t\tfor key, value := range tc.headers {\n\t\t\t\treq.Header.Set(key, value)\n\t\t\t}\n\n\t\t\trecorder := httptest.NewRecorder()\n\t\t\tacr := &agentConnectRequest{\n\t\t\t\thub: hub,\n\t\t\t\treq: req,\n\t\t\t\tres: recorder,\n\t\t\t}\n\t\t\terr = acr.agentConnect()\n\n\t\t\tassert.Equal(t, tc.expectedStatus, recorder.Code, tc.description)\n\t\t})\n\t}\n}\n\n// TestAgentWebSocketIntegration tests WebSocket connection scenarios with an actual agent\nfunc TestAgentWebSocketIntegration(t *testing.T) {\n\t// Create hub and test app\n\thub, testApp, err := createTestHub(t)\n\trequire.NoError(t, err)\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Get the hub's SSH key\n\thubSigner, err := hub.GetSSHKey(\"\")\n\trequire.NoError(t, err)\n\tgoodPubKey := hubSigner.PublicKey()\n\n\t// Generate bad key pair (should be rejected)\n\t_, badPrivKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\tbadPubKey, err := ssh.NewPublicKey(badPrivKey.Public().(ed25519.PublicKey))\n\trequire.NoError(t, err)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\trequire.NoError(t, err)\n\n\t// Create HTTP server with the actual API route\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/api/beszel/agent-connect\" {\n\t\t\tacr := &agentConnectRequest{\n\t\t\t\thub: hub,\n\t\t\t\treq: r,\n\t\t\t\tres: w,\n\t\t\t}\n\t\t\tacr.agentConnect()\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\tdefer ts.Close()\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tagentToken         string // Token agent will send\n\t\tdbToken            string // Token in database (empty means no record created)\n\t\tagentFingerprint   string // Fingerprint agent will send (empty means agent generates its own)\n\t\tdbFingerprint      string // Fingerprint in database\n\t\tagentSSHKey        ssh.PublicKey\n\t\texpectConnection   bool\n\t\texpectFingerprint  string // \"empty\", \"unchanged\", or \"updated\"\n\t\texpectSystemStatus string\n\t\tdescription        string\n\t}{\n\t\t{\n\t\t\tname:               \"empty fingerprint - agent sets fingerprint on first connection\",\n\t\t\tagentToken:         \"test-token-1\",\n\t\t\tdbToken:            \"test-token-1\",\n\t\t\tagentFingerprint:   \"agent-fingerprint-1\",\n\t\t\tdbFingerprint:      \"\",\n\t\t\tagentSSHKey:        goodPubKey,\n\t\t\texpectConnection:   true,\n\t\t\texpectFingerprint:  \"updated\",\n\t\t\texpectSystemStatus: \"up\",\n\t\t\tdescription:        \"Agent should connect and set its fingerprint when DB fingerprint is empty\",\n\t\t},\n\t\t{\n\t\t\tname:               \"matching fingerprint should be accepted\",\n\t\t\tagentToken:         \"test-token-2\",\n\t\t\tdbToken:            \"test-token-2\",\n\t\t\tagentFingerprint:   \"matching-fingerprint-123\",\n\t\t\tdbFingerprint:      \"matching-fingerprint-123\",\n\t\t\tagentSSHKey:        goodPubKey,\n\t\t\texpectConnection:   true,\n\t\t\texpectFingerprint:  \"unchanged\",\n\t\t\texpectSystemStatus: \"up\",\n\t\t\tdescription:        \"Agent should connect when its fingerprint matches existing DB fingerprint\",\n\t\t},\n\t\t{\n\t\t\tname:               \"fingerprint mismatch should be rejected\",\n\t\t\tagentToken:         \"test-token-3\",\n\t\t\tdbToken:            \"test-token-3\",\n\t\t\tagentFingerprint:   \"different-fingerprint-456\",\n\t\t\tdbFingerprint:      \"original-fingerprint-123\",\n\t\t\tagentSSHKey:        goodPubKey,\n\t\t\texpectConnection:   false,\n\t\t\texpectFingerprint:  \"unchanged\",\n\t\t\texpectSystemStatus: \"pending\",\n\t\t\tdescription:        \"Agent should be rejected when its fingerprint doesn't match existing DB fingerprint\",\n\t\t},\n\t\t{\n\t\t\tname:               \"invalid token should be rejected\",\n\t\t\tagentToken:         \"invalid-token-999\",\n\t\t\tdbToken:            \"test-token-4\",\n\t\t\tagentFingerprint:   \"matching-fingerprint-456\",\n\t\t\tdbFingerprint:      \"matching-fingerprint-456\",\n\t\t\tagentSSHKey:        goodPubKey,\n\t\t\texpectConnection:   false,\n\t\t\texpectFingerprint:  \"unchanged\",\n\t\t\texpectSystemStatus: \"pending\",\n\t\t\tdescription:        \"Connection should fail when using invalid token\",\n\t\t},\n\t\t{\n\t\t\t// This is more for the agent side, but might as well test it here\n\t\t\tname:               \"wrong SSH key should be rejected\",\n\t\t\tagentToken:         \"test-token-5\",\n\t\t\tdbToken:            \"test-token-5\",\n\t\t\tagentFingerprint:   \"matching-fingerprint-789\",\n\t\t\tdbFingerprint:      \"matching-fingerprint-789\",\n\t\t\tagentSSHKey:        badPubKey,\n\t\t\texpectConnection:   false,\n\t\t\texpectFingerprint:  \"unchanged\",\n\t\t\texpectSystemStatus: \"pending\",\n\t\t\tdescription:        \"Connection should fail when agent uses wrong SSH key\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create test system with unique port for each test\n\t\t\tportNum := 45000 + len(tc.name) // Use name length to get unique port\n\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\"name\":   fmt.Sprintf(\"test-system-%s\", tc.name),\n\t\t\t\t\"host\":   \"localhost\",\n\t\t\t\t\"port\":   fmt.Sprintf(\"%d\", portNum),\n\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Always create fingerprint record for this test's system\n\t\t\tfingerprintRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\"token\":       tc.dbToken,\n\t\t\t\t\"fingerprint\": tc.dbFingerprint,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create and configure agent\n\t\t\tagentDataDir := t.TempDir()\n\n\t\t\t// Set up agent fingerprint if specified\n\t\t\terr = os.WriteFile(filepath.Join(agentDataDir, \"fingerprint\"), []byte(tc.agentFingerprint), 0644)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Logf(\"Pre-created fingerprint file for agent: %s\", tc.agentFingerprint)\n\n\t\t\ttestAgent, err := agent.NewAgent(agentDataDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Set up environment variables for the agent\n\t\t\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", ts.URL)\n\t\t\tos.Setenv(\"BESZEL_AGENT_TOKEN\", tc.agentToken)\n\t\t\tdefer func() {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\t\t}()\n\n\t\t\t// Start agent in background\n\t\t\tdone := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\tserverOptions := agent.ServerOptions{\n\t\t\t\t\tNetwork: \"tcp\",\n\t\t\t\t\tAddr:    fmt.Sprintf(\"127.0.0.1:%d\", portNum),\n\t\t\t\t\tKeys:    []ssh.PublicKey{tc.agentSSHKey},\n\t\t\t\t}\n\t\t\t\tdone <- testAgent.Start(serverOptions)\n\t\t\t}()\n\n\t\t\t// Wait for connection result\n\t\t\tmaxWait := 2 * time.Second\n\t\t\ttime.Sleep(40 * time.Millisecond)\n\t\t\tcheckInterval := 20 * time.Millisecond\n\t\t\ttimeout := time.After(maxWait)\n\t\t\tticker := time.Tick(checkInterval)\n\n\t\t\tconnectionManager := testAgent.GetConnectionManager()\n\n\t\t\tconnectionResult := false\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-timeout:\n\t\t\t\t\t// Timeout reached\n\t\t\t\t\tif tc.expectConnection {\n\t\t\t\t\t\tt.Fatalf(\"Expected connection to succeed but timed out - agent state: %d\", connectionManager.State)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Logf(\"Connection properly rejected (timeout) - agent state: %d\", connectionManager.State)\n\t\t\t\t\t}\n\t\t\t\t\tconnectionResult = false\n\t\t\t\tcase <-ticker:\n\t\t\t\t\tif connectionManager.State == agent.WebSocketConnected {\n\t\t\t\t\t\tif tc.expectConnection {\n\t\t\t\t\t\t\tt.Logf(\"WebSocket connection successful - agent state: %d\", connectionManager.State)\n\t\t\t\t\t\t\tconnectionResult = true\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tt.Errorf(\"Unexpected: Connection succeeded when it should have been rejected\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase err := <-done:\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tif !tc.expectConnection {\n\t\t\t\t\t\t\tt.Logf(\"Agent connection properly rejected: %v\", err)\n\t\t\t\t\t\t\tconnectionResult = false\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tt.Fatalf(\"Agent failed to start: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Break if we got the expected result or timed out\n\t\t\t\tif connectionResult == tc.expectConnection || connectionResult {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\n\t\t\t// Verify fingerprint state by re-reading the specific record\n\t\t\tupdatedFingerprintRecord, err := testApp.FindRecordById(\"fingerprints\", fingerprintRecord.Id)\n\t\t\trequire.NoError(t, err)\n\t\t\tfinalFingerprint := updatedFingerprintRecord.GetString(\"fingerprint\")\n\n\t\t\tswitch tc.expectFingerprint {\n\t\t\tcase \"empty\":\n\t\t\t\tassert.Empty(t, finalFingerprint, \"Fingerprint should be empty\")\n\t\t\tcase \"unchanged\":\n\t\t\t\tassert.Equal(t, tc.dbFingerprint, finalFingerprint, \"Fingerprint should not change when connection is rejected\")\n\t\t\tcase \"updated\":\n\t\t\t\tif tc.dbFingerprint == \"\" {\n\t\t\t\t\tassert.NotEmpty(t, finalFingerprint, \"Fingerprint should be updated after successful connection\")\n\t\t\t\t} else {\n\t\t\t\t\tassert.NotEqual(t, tc.dbFingerprint, finalFingerprint, \"Fingerprint should be updated after successful connection\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify system status\n\t\t\tupdatedSystemRecord, err := testApp.FindRecordById(\"systems\", systemRecord.Id)\n\t\t\trequire.NoError(t, err)\n\t\t\tstatus := updatedSystemRecord.GetString(\"status\")\n\t\t\tassert.Equal(t, tc.expectSystemStatus, status, \"System status should match expected value\")\n\n\t\t\tt.Logf(\"%s - System status: %s, Fingerprint: %s\", tc.description, status, finalFingerprint)\n\t\t})\n\t}\n}\n\n// TestMultipleSystemsWithSameUniversalToken tests that multiple systems can share the same universal token\nfunc TestMultipleSystemsWithSameUniversalToken(t *testing.T) {\n\t// Create hub and test app\n\thub, testApp, err := createTestHub(t)\n\trequire.NoError(t, err)\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Get the hub's SSH key\n\thubSigner, err := hub.GetSSHKey(\"\")\n\trequire.NoError(t, err)\n\tgoodPubKey := hubSigner.PublicKey()\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\trequire.NoError(t, err)\n\n\t// Set up universal token in the token map\n\tuniversalToken := \"shared-universal-token-123\"\n\tuniversalTokenMap.GetMap().Set(universalToken, userRecord.Id, time.Hour)\n\n\t// Create HTTP server with the actual API route\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/api/beszel/agent-connect\" {\n\t\t\tacr := &agentConnectRequest{\n\t\t\t\thub: hub,\n\t\t\t\treq: r,\n\t\t\t\tres: w,\n\t\t\t}\n\t\t\tacr.agentConnect()\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\tdefer ts.Close()\n\n\t// Test scenarios for universal tokens\n\ttestCases := []struct {\n\t\tname               string\n\t\tagentFingerprint   string\n\t\texpectConnection   bool\n\t\texpectSystemStatus string\n\t\texpectNewSystem    bool // Whether we expect a new system to be created\n\t\tdescription        string\n\t}{\n\t\t{\n\t\t\tname:               \"first system with universal token\",\n\t\t\tagentFingerprint:   \"system-1-fingerprint\",\n\t\t\texpectConnection:   true,\n\t\t\texpectSystemStatus: \"up\",\n\t\t\texpectNewSystem:    true,\n\t\t\tdescription:        \"First system should create a new system\",\n\t\t},\n\t\t{\n\t\t\tname:               \"same system reconnecting with same fingerprint\",\n\t\t\tagentFingerprint:   \"system-1-fingerprint\", // Same fingerprint as first\n\t\t\texpectConnection:   true,\n\t\t\texpectSystemStatus: \"up\",\n\t\t\texpectNewSystem:    false, // Should reuse existing system\n\t\t\tdescription:        \"Same system should reuse existing system record\",\n\t\t},\n\t\t{\n\t\t\tname:               \"different system with same universal token\",\n\t\t\tagentFingerprint:   \"system-2-fingerprint\", // Different fingerprint\n\t\t\texpectConnection:   true,\n\t\t\texpectSystemStatus: \"up\",\n\t\t\texpectNewSystem:    true, // Should create new system\n\t\t\tdescription:        \"Different system should create a new system record\",\n\t\t},\n\t}\n\n\tvar systemCount int\n\tfor i, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create unique port for each test\n\t\t\tportNum := 46000 + i\n\n\t\t\t// Create and configure agent\n\t\t\tagentDataDir := t.TempDir()\n\n\t\t\t// Set up agent fingerprint\n\t\t\terr = os.WriteFile(filepath.Join(agentDataDir, \"fingerprint\"), []byte(tc.agentFingerprint), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttestAgent, err := agent.NewAgent(agentDataDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Set up environment variables for the agent\n\t\t\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", ts.URL)\n\t\t\tos.Setenv(\"BESZEL_AGENT_TOKEN\", universalToken)\n\t\t\tdefer func() {\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\t\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t\t\t}()\n\n\t\t\t// Count systems before connection\n\t\t\tsystemsBefore, err := testApp.FindRecordsByFilter(\"systems\", \"users ~ {:userId}\", \"\", -1, 0, map[string]any{\"userId\": userRecord.Id})\n\t\t\trequire.NoError(t, err)\n\t\t\tsystemsBeforeCount := len(systemsBefore)\n\n\t\t\t// Start agent in background\n\t\t\tdone := make(chan error, 1)\n\t\t\tgo func() {\n\t\t\t\tserverOptions := agent.ServerOptions{\n\t\t\t\t\tNetwork: \"tcp\",\n\t\t\t\t\tAddr:    fmt.Sprintf(\"127.0.0.1:%d\", portNum),\n\t\t\t\t\tKeys:    []ssh.PublicKey{goodPubKey},\n\t\t\t\t}\n\t\t\t\tdone <- testAgent.Start(serverOptions)\n\t\t\t}()\n\n\t\t\t// Wait for connection result\n\t\t\tmaxWait := 2 * time.Second\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\tcheckInterval := 20 * time.Millisecond\n\t\t\ttimeout := time.After(maxWait)\n\t\t\tticker := time.Tick(checkInterval)\n\n\t\t\tconnectionManager := testAgent.GetConnectionManager()\n\t\t\tconnectionResult := false\n\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-timeout:\n\t\t\t\t\tif tc.expectConnection {\n\t\t\t\t\t\tt.Fatalf(\"Expected connection to succeed but timed out - agent state: %d\", connectionManager.State)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Logf(\"Connection properly rejected (timeout) - agent state: %d\", connectionManager.State)\n\t\t\t\t\t}\n\t\t\t\t\tconnectionResult = false\n\t\t\t\tcase <-ticker:\n\t\t\t\t\tif connectionManager.State == agent.WebSocketConnected {\n\t\t\t\t\t\tif tc.expectConnection {\n\t\t\t\t\t\t\tt.Logf(\"WebSocket connection successful - agent state: %d\", connectionManager.State)\n\t\t\t\t\t\t\tconnectionResult = true\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tt.Errorf(\"Unexpected: Connection succeeded when it should have been rejected\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase err := <-done:\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tif !tc.expectConnection {\n\t\t\t\t\t\t\tt.Logf(\"Agent connection properly rejected: %v\", err)\n\t\t\t\t\t\t\tconnectionResult = false\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tt.Fatalf(\"Agent failed to start: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif connectionResult == tc.expectConnection || connectionResult {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify system creation/reuse behavior\n\t\t\tif tc.expectConnection {\n\t\t\t\t// Count systems after connection\n\t\t\t\tsystemsAfter, err := testApp.FindRecordsByFilter(\"systems\", \"users ~ {:userId}\", \"\", -1, 0, map[string]any{\"userId\": userRecord.Id})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tsystemsAfterCount := len(systemsAfter)\n\n\t\t\t\tif tc.expectNewSystem {\n\t\t\t\t\t// Should have created a new system\n\t\t\t\t\tsystemCount++\n\t\t\t\t\tassert.Equal(t, systemsBeforeCount+1, systemsAfterCount, \"Should have created a new system\")\n\t\t\t\t\tassert.Equal(t, systemCount, systemsAfterCount, \"Total system count should match expected\")\n\t\t\t\t} else {\n\t\t\t\t\t// Should have reused existing system\n\t\t\t\t\tassert.Equal(t, systemsBeforeCount, systemsAfterCount, \"Should not have created a new system\")\n\t\t\t\t\tassert.Equal(t, systemCount, systemsAfterCount, \"Total system count should remain the same\")\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(20 * time.Millisecond)\n\n\t\t\t\t// Verify that a fingerprint record exists for this fingerprint\n\t\t\t\tfingerprints, err := testApp.FindRecordsByFilter(\"fingerprints\", \"token = {:token} && fingerprint = {:fingerprint}\", \"\", -1, 0, map[string]any{\n\t\t\t\t\t\"token\":       universalToken,\n\t\t\t\t\t\"fingerprint\": tc.agentFingerprint,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Len(t, fingerprints, 1, \"Should have exactly one fingerprint record for this token+fingerprint combination\")\n\n\t\t\t\tfingerprint := fingerprints[0]\n\t\t\t\tassert.Equal(t, universalToken, fingerprint.GetString(\"token\"), \"Fingerprint should have the universal token\")\n\t\t\t\tassert.Equal(t, tc.agentFingerprint, fingerprint.GetString(\"fingerprint\"), \"Fingerprint should match agent's fingerprint\")\n\n\t\t\t\t// Verify system status\n\t\t\t\tsystemId := fingerprint.GetString(\"system\")\n\t\t\t\tsystem, err := testApp.FindRecordById(\"systems\", systemId)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tstatus := system.GetString(\"status\")\n\t\t\t\tassert.Equal(t, tc.expectSystemStatus, status, \"System status should match expected value\")\n\n\t\t\t\tt.Logf(\"%s - System ID: %s, Status: %s, New System: %v\", tc.description, systemId, status, tc.expectNewSystem)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestPermanentUniversalTokenFromDB verifies that a universal token persisted in the DB\n// (universal_tokens collection) is accepted for agent self-registration even if it is not\n// present in the in-memory universalTokenMap.\nfunc TestPermanentUniversalTokenFromDB(t *testing.T) {\n\t// Create hub and test app\n\thub, testApp, err := createTestHub(t)\n\trequire.NoError(t, err)\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Get the hub's SSH key\n\thubSigner, err := hub.GetSSHKey(\"\")\n\trequire.NoError(t, err)\n\tgoodPubKey := hubSigner.PublicKey()\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\trequire.NoError(t, err)\n\n\t// Create a permanent universal token record in the DB (do NOT add it to universalTokenMap)\n\tuniversalToken := \"db-universal-token-123\"\n\t_, err = createTestRecord(testApp, \"universal_tokens\", map[string]any{\n\t\t\"user\":  userRecord.Id,\n\t\t\"token\": universalToken,\n\t})\n\trequire.NoError(t, err)\n\n\t// Create HTTP server with the actual API route\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/api/beszel/agent-connect\" {\n\t\t\tacr := &agentConnectRequest{\n\t\t\t\thub: hub,\n\t\t\t\treq: r,\n\t\t\t\tres: w,\n\t\t\t}\n\t\t\tacr.agentConnect()\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\tdefer ts.Close()\n\n\t// Create and configure agent\n\tagentDataDir := t.TempDir()\n\terr = os.WriteFile(filepath.Join(agentDataDir, \"fingerprint\"), []byte(\"db-token-system-fingerprint\"), 0644)\n\trequire.NoError(t, err)\n\n\ttestAgent, err := agent.NewAgent(agentDataDir)\n\trequire.NoError(t, err)\n\n\t// Set up environment variables for the agent\n\tos.Setenv(\"BESZEL_AGENT_HUB_URL\", ts.URL)\n\tos.Setenv(\"BESZEL_AGENT_TOKEN\", universalToken)\n\tdefer func() {\n\t\tos.Unsetenv(\"BESZEL_AGENT_HUB_URL\")\n\t\tos.Unsetenv(\"BESZEL_AGENT_TOKEN\")\n\t}()\n\n\t// Start agent in background\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tserverOptions := agent.ServerOptions{\n\t\t\tNetwork: \"tcp\",\n\t\t\tAddr:    \"127.0.0.1:46050\",\n\t\t\tKeys:    []ssh.PublicKey{goodPubKey},\n\t\t}\n\t\tdone <- testAgent.Start(serverOptions)\n\t}()\n\n\t// Wait for connection result\n\tmaxWait := 2 * time.Second\n\ttime.Sleep(20 * time.Millisecond)\n\tcheckInterval := 20 * time.Millisecond\n\ttimeout := time.After(maxWait)\n\tticker := time.Tick(checkInterval)\n\n\tconnectionManager := testAgent.GetConnectionManager()\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\tt.Fatalf(\"Expected connection to succeed but timed out - agent state: %d\", connectionManager.State)\n\t\tcase <-ticker:\n\t\t\tif connectionManager.State == agent.WebSocketConnected {\n\t\t\t\t// Success\n\t\t\t\tgoto verify\n\t\t\t}\n\t\tcase err := <-done:\n\t\t\t// If Start returns early, treat it as failure\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Agent failed to start/connect: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\nverify:\n\t// Verify that a system was created for the user (self-registration path)\n\tsystemsAfter, err := testApp.FindRecordsByFilter(\"systems\", \"users ~ {:userId}\", \"\", -1, 0, map[string]any{\"userId\": userRecord.Id})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, systemsAfter, \"Expected a system to be created for DB-backed universal token\")\n}\n\n// TestFindOrCreateSystemForToken tests the findOrCreateSystemForToken function\nfunc TestFindOrCreateSystemForToken(t *testing.T) {\n\thub, testApp, err := createTestHub(t)\n\trequire.NoError(t, err)\n\tdefer cleanupTestHub(hub, testApp)\n\n\t// Create test user\n\tuserRecord, err := createTestUser(testApp)\n\trequire.NoError(t, err)\n\n\ttype testCase struct {\n\t\tname                string\n\t\tsetup               func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord)\n\t\tagentFingerprint    common.FingerprintResponse\n\t\texpectError         bool\n\t\texpectNewSystem     bool\n\t\texpectedFingerprint string\n\t\tdescription         string\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tname: \"universal token - existing fingerprint match\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test system\n\t\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"existing-system\",\n\t\t\t\t\t\"host\":   \"192.168.1.100\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint record\n\t\t\t\tfpRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\t\"token\":       \"universal-token-123\",\n\t\t\t\t\t\"fingerprint\": \"existing-fingerprint\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"universal-token-123\",\n\t\t\t\t\tisUniversalToken: true,\n\t\t\t\t\tuserId:           userRecord.Id,\n\t\t\t\t\treq: &http.Request{\n\t\t\t\t\t\tRemoteAddr: \"192.168.1.100\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord.Id,\n\t\t\t\t\t\tSystemId:    systemRecord.Id,\n\t\t\t\t\t\tFingerprint: \"existing-fingerprint\",\n\t\t\t\t\t\tToken:       \"universal-token-123\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"existing-fingerprint\",\n\t\t\t\tHostname:    \"test-host\",\n\t\t\t\tPort:        \"8080\",\n\t\t\t},\n\t\t\texpectError:         false,\n\t\t\texpectNewSystem:     false,\n\t\t\texpectedFingerprint: \"existing-fingerprint\",\n\t\t\tdescription:         \"Should reuse existing system with matching fingerprint\",\n\t\t},\n\t\t{\n\t\t\tname: \"universal token - new fingerprint\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test system\n\t\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"existing-system-2\",\n\t\t\t\t\t\"host\":   \"192.168.1.101\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint record\n\t\t\t\tfpRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\t\"token\":       \"universal-token-123\",\n\t\t\t\t\t\"fingerprint\": \"existing-fingerprint\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"universal-token-123\",\n\t\t\t\t\tisUniversalToken: true,\n\t\t\t\t\tuserId:           userRecord.Id,\n\t\t\t\t\treq: &http.Request{\n\t\t\t\t\t\tRemoteAddr: \"192.168.1.200\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord.Id,\n\t\t\t\t\t\tSystemId:    systemRecord.Id,\n\t\t\t\t\t\tFingerprint: \"existing-fingerprint\",\n\t\t\t\t\t\tToken:       \"universal-token-123\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"new-fingerprint\",\n\t\t\t\tHostname:    \"new-host\",\n\t\t\t\tPort:        \"9090\",\n\t\t\t},\n\t\t\texpectError:         false,\n\t\t\texpectNewSystem:     true,\n\t\t\texpectedFingerprint: \"new-fingerprint\",\n\t\t\tdescription:         \"Should create new system with different fingerprint\",\n\t\t},\n\t\t{\n\t\t\tname: \"universal token - no existing records\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"universal-token-456\",\n\t\t\t\t\tisUniversalToken: true,\n\t\t\t\t\tuserId:           userRecord.Id,\n\t\t\t\t\treq: &http.Request{\n\t\t\t\t\t\tRemoteAddr: \"192.168.1.300\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"first-fingerprint\",\n\t\t\t\tHostname:    \"first-host\",\n\t\t\t\tPort:        \"7070\",\n\t\t\t},\n\t\t\texpectError:         false,\n\t\t\texpectNewSystem:     true,\n\t\t\texpectedFingerprint: \"first-fingerprint\",\n\t\t\tdescription:         \"Should create new system when no existing records\",\n\t\t},\n\t\t{\n\t\t\tname: \"regular token - empty fingerprint\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test system\n\t\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"regular-system\",\n\t\t\t\t\t\"host\":   \"192.168.1.200\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint record with empty fingerprint\n\t\t\t\tfpRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\t\"token\":       \"regular-token-123\",\n\t\t\t\t\t\"fingerprint\": \"\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"regular-token-123\",\n\t\t\t\t\tisUniversalToken: false,\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord.Id,\n\t\t\t\t\t\tSystemId:    systemRecord.Id,\n\t\t\t\t\t\tFingerprint: \"\",\n\t\t\t\t\t\tToken:       \"regular-token-123\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"agent-fingerprint\",\n\t\t\t\tHostname:    \"agent-host\",\n\t\t\t\tPort:        \"6060\",\n\t\t\t},\n\t\t\texpectError:         false,\n\t\t\texpectNewSystem:     false,\n\t\t\texpectedFingerprint: \"agent-fingerprint\",\n\t\t\tdescription:         \"Should update empty fingerprint for regular token\",\n\t\t},\n\t\t{\n\t\t\tname: \"regular token - fingerprint mismatch\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test system\n\t\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"regular-system-2\",\n\t\t\t\t\t\"host\":   \"192.168.1.250\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint record with different fingerprint\n\t\t\t\tfpRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\t\"token\":       \"regular-token-456\",\n\t\t\t\t\t\"fingerprint\": \"different-fingerprint\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"regular-token-456\",\n\t\t\t\t\tisUniversalToken: false,\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord.Id,\n\t\t\t\t\t\tSystemId:    systemRecord.Id,\n\t\t\t\t\t\tFingerprint: \"different-fingerprint\",\n\t\t\t\t\t\tToken:       \"regular-token-456\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"agent-fingerprint\",\n\t\t\t\tHostname:    \"agent-host\",\n\t\t\t\tPort:        \"5050\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"Should reject fingerprint mismatch for regular token\",\n\t\t},\n\t\t{\n\t\t\tname: \"universal token - missing user ID\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"universal-token-789\",\n\t\t\t\t\tisUniversalToken: true,\n\t\t\t\t\tuserId:           \"\", // Missing user ID\n\t\t\t\t\treq: &http.Request{\n\t\t\t\t\t\tRemoteAddr: \"192.168.1.400\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"some-fingerprint\",\n\t\t\t\tHostname:    \"some-host\",\n\t\t\t\tPort:        \"4040\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"Should reject universal token without user ID\",\n\t\t},\n\t\t{\n\t\t\tname: \"expired universal token - matching fingerprint\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test systems\n\t\t\t\tsystemRecord1, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"expired-system-1\",\n\t\t\t\t\t\"host\":   \"192.168.1.500\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tsystemRecord2, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"expired-system-2\",\n\t\t\t\t\t\"host\":   \"192.168.1.501\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint records\n\t\t\t\tfpRecord1, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord1.Id,\n\t\t\t\t\t\"token\":       \"expired-universal-token-123\",\n\t\t\t\t\t\"fingerprint\": \"expired-fingerprint-1\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfpRecord2, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord2.Id,\n\t\t\t\t\t\"token\":       \"expired-universal-token-123\",\n\t\t\t\t\t\"fingerprint\": \"expired-fingerprint-2\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"expired-universal-token-123\",\n\t\t\t\t\tisUniversalToken: false, // Token is no longer active\n\t\t\t\t\tuserId:           \"\",    // No user ID since token is expired\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord1.Id,\n\t\t\t\t\t\tSystemId:    systemRecord1.Id,\n\t\t\t\t\t\tFingerprint: \"expired-fingerprint-1\",\n\t\t\t\t\t\tToken:       \"expired-universal-token-123\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord2.Id,\n\t\t\t\t\t\tSystemId:    systemRecord2.Id,\n\t\t\t\t\t\tFingerprint: \"expired-fingerprint-2\",\n\t\t\t\t\t\tToken:       \"expired-universal-token-123\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"expired-fingerprint-1\", // Matches first record\n\t\t\t\tHostname:    \"expired-host\",\n\t\t\t\tPort:        \"3030\",\n\t\t\t},\n\t\t\texpectError:         false,\n\t\t\texpectNewSystem:     false,\n\t\t\texpectedFingerprint: \"expired-fingerprint-1\",\n\t\t\tdescription:         \"Should allow connection with expired universal token if fingerprint matches\",\n\t\t},\n\t\t{\n\t\t\tname: \"expired universal token - no matching fingerprint\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\t// Create test system\n\t\t\t\tsystemRecord, err := createTestRecord(testApp, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":   \"expired-system-3\",\n\t\t\t\t\t\"host\":   \"192.168.1.600\",\n\t\t\t\t\t\"port\":   \"45876\",\n\t\t\t\t\t\"status\": \"pending\",\n\t\t\t\t\t\"users\":  []string{userRecord.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create fingerprint record\n\t\t\t\tfpRecord, err := createTestRecord(testApp, \"fingerprints\", map[string]any{\n\t\t\t\t\t\"system\":      systemRecord.Id,\n\t\t\t\t\t\"token\":       \"expired-universal-token-456\",\n\t\t\t\t\t\"fingerprint\": \"expired-fingerprint-3\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"expired-universal-token-456\",\n\t\t\t\t\tisUniversalToken: false, // Token is no longer active\n\t\t\t\t\tuserId:           \"\",    // No user ID since token is expired\n\t\t\t\t\treq: &http.Request{\n\t\t\t\t\t\tRemoteAddr: \"192.168.1.600\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfpRecords := []ws.FingerprintRecord{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:          fpRecord.Id,\n\t\t\t\t\t\tSystemId:    systemRecord.Id,\n\t\t\t\t\t\tFingerprint: \"expired-fingerprint-3\",\n\t\t\t\t\t\tToken:       \"expired-universal-token-456\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\treturn acr, fpRecords\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"different-fingerprint\", // Doesn't match any existing record\n\t\t\t\tHostname:    \"different-host\",\n\t\t\t\tPort:        \"2020\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"Should reject connection with expired universal token if no fingerprint matches\",\n\t\t},\n\t\t{\n\t\t\tname: \"regular token - no existing records\",\n\t\t\tsetup: func(t *testing.T, hub *Hub, testApp *pbtests.TestApp, userRecord *core.Record) (agentConnectRequest, []ws.FingerprintRecord) {\n\t\t\t\tacr := agentConnectRequest{\n\t\t\t\t\thub:              hub,\n\t\t\t\t\ttoken:            \"regular-token-no-record\",\n\t\t\t\t\tisUniversalToken: false,\n\t\t\t\t}\n\t\t\t\treturn acr, []ws.FingerprintRecord{}\n\t\t\t},\n\t\t\tagentFingerprint: common.FingerprintResponse{\n\t\t\t\tFingerprint: \"some-fingerprint\",\n\t\t\t},\n\t\t\texpectError: true,\n\t\t\tdescription: \"Should reject regular token with no fingerprint record\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tacr, fpRecords := tc.setup(t, hub, testApp, userRecord)\n\t\t\tresult, err := acr.findOrCreateSystemForToken(fpRecords, tc.agentFingerprint)\n\n\t\t\tif tc.expectError {\n\t\t\t\tassert.Error(t, err, tc.description)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, tc.description)\n\n\t\t\t// Verify expected fingerprint\n\t\t\tif tc.expectedFingerprint != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectedFingerprint, result.Fingerprint, \"Fingerprint should match expected\")\n\t\t\t}\n\n\t\t\t// For new systems, verify they were actually created\n\t\t\tif tc.expectNewSystem {\n\t\t\t\tassert.NotEmpty(t, result.SystemId, \"New system should have a system ID\")\n\n\t\t\t\t// Verify system was created in database\n\t\t\t\tsystem, err := testApp.FindRecordById(\"systems\", result.SystemId)\n\t\t\t\trequire.NoError(t, err, \"New system should exist in database\")\n\n\t\t\t\t// Verify system properties\n\t\t\t\tassert.Equal(t, tc.agentFingerprint.Hostname, system.GetString(\"name\"), \"System name should match hostname\")\n\t\t\t\tassert.Equal(t, getRealIP(acr.req), system.GetString(\"host\"), \"System host should match remote address\")\n\t\t\t\tassert.Equal(t, tc.agentFingerprint.Port, system.GetString(\"port\"), \"System port should match agent port\")\n\t\t\t\tassert.Equal(t, []string{acr.userId}, system.Get(\"users\"), \"System users should match\")\n\t\t\t}\n\n\t\t\tt.Logf(\"%s - Result: SystemId=%s, Fingerprint=%s\", tc.description, result.SystemId, result.Fingerprint)\n\t\t})\n\t}\n}\n\n// TestGetRealIP tests the getRealIP function\nfunc TestGetRealIP(t *testing.T) {\n\ttestCases := []struct {\n\t\tname       string\n\t\theaders    map[string]string\n\t\tremoteAddr string\n\t\texpectedIP string\n\t}{\n\t\t{\n\t\t\tname:       \"CF-Connecting-IP header\",\n\t\t\theaders:    map[string]string{\"CF-Connecting-IP\": \"192.168.1.1\"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"X-Forwarded-For header with single IP\",\n\t\t\theaders:    map[string]string{\"X-Forwarded-For\": \"192.168.1.2\"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.2\",\n\t\t},\n\t\t{\n\t\t\tname:       \"X-Forwarded-For header with multiple IPs\",\n\t\t\theaders:    map[string]string{\"X-Forwarded-For\": \"192.168.1.3, 10.0.0.1, 172.16.0.1\"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.3\",\n\t\t},\n\t\t{\n\t\t\tname:       \"X-Forwarded-For header with spaces\",\n\t\t\theaders:    map[string]string{\"X-Forwarded-For\": \"  192.168.1.4  \"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.4\",\n\t\t},\n\t\t{\n\t\t\tname:       \"No headers, fallback to RemoteAddr with port\",\n\t\t\theaders:    map[string]string{},\n\t\t\tremoteAddr: \"192.168.1.5:54321\",\n\t\t\texpectedIP: \"192.168.1.5\",\n\t\t},\n\t\t{\n\t\t\tname:       \"No headers, fallback to RemoteAddr without port\",\n\t\t\theaders:    map[string]string{},\n\t\t\tremoteAddr: \"192.168.1.6\",\n\t\t\texpectedIP: \"192.168.1.6\",\n\t\t},\n\t\t{\n\t\t\tname:       \"Both headers present, CF takes precedence\",\n\t\t\theaders:    map[string]string{\"CF-Connecting-IP\": \"192.168.1.1\", \"X-Forwarded-For\": \"192.168.1.2\"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.1\",\n\t\t},\n\t\t{\n\t\t\tname:       \"X-Forwarded-For present, takes precedence over RemoteAddr\",\n\t\t\theaders:    map[string]string{\"X-Forwarded-For\": \"192.168.1.2\"},\n\t\t\tremoteAddr: \"192.168.1.5:54321\",\n\t\t\texpectedIP: \"192.168.1.2\",\n\t\t},\n\t\t{\n\t\t\tname:       \"Empty X-Forwarded-For, fallback to RemoteAddr\",\n\t\t\theaders:    map[string]string{\"X-Forwarded-For\": \"\"},\n\t\t\tremoteAddr: \"192.168.1.7:12345\",\n\t\t\texpectedIP: \"192.168.1.7\",\n\t\t},\n\t\t{\n\t\t\tname:       \"Empty CF-Connecting-IP, fallback to X-Forwarded-For\",\n\t\t\theaders:    map[string]string{\"CF-Connecting-IP\": \"\", \"X-Forwarded-For\": \"192.168.1.8\"},\n\t\t\tremoteAddr: \"127.0.0.1:12345\",\n\t\t\texpectedIP: \"192.168.1.8\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tfor key, value := range tc.headers {\n\t\t\t\treq.Header.Set(key, value)\n\t\t\t}\n\t\t\treq.RemoteAddr = tc.remoteAddr\n\n\t\t\tip := getRealIP(req)\n\t\t\tassert.Equal(t, tc.expectedIP, ip)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/hub/config/config.go",
    "content": "// Package config provides functions for syncing systems with the config.yml file\npackage config\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/spf13/cast\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype config struct {\n\tSystems []systemConfig `yaml:\"systems\"`\n}\n\ntype systemConfig struct {\n\tName  string   `yaml:\"name\"`\n\tHost  string   `yaml:\"host\"`\n\tPort  uint16   `yaml:\"port,omitempty\"`\n\tToken string   `yaml:\"token,omitempty\"`\n\tUsers []string `yaml:\"users\"`\n}\n\n// Syncs systems with the config.yml file\nfunc SyncSystems(e *core.ServeEvent) error {\n\th := e.App\n\tconfigPath := filepath.Join(h.DataDir(), \"config.yml\")\n\tconfigData, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tvar config config\n\terr = yaml.Unmarshal(configData, &config)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse config.yml: %v\", err)\n\t}\n\n\tif len(config.Systems) == 0 {\n\t\tlog.Println(\"No systems defined in config.yml.\")\n\t\treturn nil\n\t}\n\n\tvar firstUser *core.Record\n\n\t// Create a map of email to user ID\n\tuserEmailToID := make(map[string]string)\n\tusers, err := h.FindAllRecords(\"users\", dbx.NewExp(\"id != ''\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(users) > 0 {\n\t\tfirstUser = users[0]\n\t\tfor _, user := range users {\n\t\t\tuserEmailToID[user.GetString(\"email\")] = user.Id\n\t\t}\n\t}\n\n\t// add default settings for systems if not defined in config\n\tfor i := range config.Systems {\n\t\tsystem := &config.Systems[i]\n\t\tif system.Port == 0 {\n\t\t\tsystem.Port = 45876\n\t\t}\n\t\tif len(users) > 0 && len(system.Users) == 0 {\n\t\t\t// default to first user if none are defined\n\t\t\tsystem.Users = []string{firstUser.Id}\n\t\t} else {\n\t\t\t// Convert email addresses to user IDs\n\t\t\tuserIDs := make([]string, 0, len(system.Users))\n\t\t\tfor _, email := range system.Users {\n\t\t\t\tif id, ok := userEmailToID[email]; ok {\n\t\t\t\t\tuserIDs = append(userIDs, id)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Printf(\"User %s not found\", email)\n\t\t\t\t}\n\t\t\t}\n\t\t\tsystem.Users = userIDs\n\t\t}\n\t}\n\n\t// Get existing systems\n\texistingSystems, err := h.FindAllRecords(\"systems\", dbx.NewExp(\"id != ''\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create a map of existing systems\n\texistingSystemsMap := make(map[string]*core.Record)\n\tfor _, system := range existingSystems {\n\t\tkey := system.GetString(\"name\") + system.GetString(\"host\") + system.GetString(\"port\")\n\t\texistingSystemsMap[key] = system\n\t}\n\n\t// Process systems from config\n\tfor _, sysConfig := range config.Systems {\n\t\tkey := sysConfig.Name + sysConfig.Host + cast.ToString(sysConfig.Port)\n\t\tif existingSystem, ok := existingSystemsMap[key]; ok {\n\t\t\t// Update existing system\n\t\t\texistingSystem.Set(\"name\", sysConfig.Name)\n\t\t\texistingSystem.Set(\"users\", sysConfig.Users)\n\t\t\texistingSystem.Set(\"port\", sysConfig.Port)\n\t\t\tif err := h.Save(existingSystem); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Only update token if one is specified in config, otherwise preserve existing token\n\t\t\tif sysConfig.Token != \"\" {\n\t\t\t\tif err := updateFingerprintToken(h, existingSystem.Id, sysConfig.Token); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdelete(existingSystemsMap, key)\n\t\t} else {\n\t\t\t// Create new system\n\t\t\tsystemsCollection, err := h.FindCollectionByNameOrId(\"systems\")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to find systems collection: %v\", err)\n\t\t\t}\n\t\t\tnewSystem := core.NewRecord(systemsCollection)\n\t\t\tnewSystem.Set(\"name\", sysConfig.Name)\n\t\t\tnewSystem.Set(\"host\", sysConfig.Host)\n\t\t\tnewSystem.Set(\"port\", sysConfig.Port)\n\t\t\tnewSystem.Set(\"users\", sysConfig.Users)\n\t\t\tnewSystem.Set(\"info\", system.Info{})\n\t\t\tnewSystem.Set(\"status\", \"pending\")\n\t\t\tif err := h.Save(newSystem); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create new system: %v\", err)\n\t\t\t}\n\n\t\t\t// For new systems, generate token if not provided\n\t\t\ttoken := sysConfig.Token\n\t\t\tif token == \"\" {\n\t\t\t\ttoken = uuid.New().String()\n\t\t\t}\n\n\t\t\t// Create fingerprint record for new system\n\t\t\tif err := createFingerprintRecord(h, newSystem.Id, token); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Delete systems not in config (and their fingerprint records will cascade delete)\n\tfor _, system := range existingSystemsMap {\n\t\tif err := h.Delete(system); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tlog.Println(\"Systems synced with config.yml\")\n\treturn nil\n}\n\n// Generates content for the config.yml file as a YAML string\nfunc generateYAML(h core.App) (string, error) {\n\t// Fetch all systems from the database\n\tsystems, err := h.FindRecordsByFilter(\"systems\", \"id != ''\", \"name\", -1, 0)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Create a Config struct to hold the data\n\tconfig := config{\n\t\tSystems: make([]systemConfig, 0, len(systems)),\n\t}\n\n\t// Fetch all users at once\n\tallUserIDs := make([]string, 0)\n\tfor _, system := range systems {\n\t\tallUserIDs = append(allUserIDs, system.GetStringSlice(\"users\")...)\n\t}\n\tuserEmailMap, err := getUserEmailMap(h, allUserIDs)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Fetch all fingerprint records to get tokens\n\ttype fingerprintData struct {\n\t\tID     string `db:\"id\"`\n\t\tSystem string `db:\"system\"`\n\t\tToken  string `db:\"token\"`\n\t}\n\tvar fingerprints []fingerprintData\n\terr = h.DB().NewQuery(\"SELECT id, system, token FROM fingerprints\").All(&fingerprints)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Create a map of system ID to token\n\tsystemTokenMap := make(map[string]string)\n\tfor _, fingerprint := range fingerprints {\n\t\tsystemTokenMap[fingerprint.System] = fingerprint.Token\n\t}\n\n\t// Populate the Config struct with system data\n\tfor _, system := range systems {\n\t\tuserIDs := system.GetStringSlice(\"users\")\n\t\tuserEmails := make([]string, 0, len(userIDs))\n\t\tfor _, userID := range userIDs {\n\t\t\tif email, ok := userEmailMap[userID]; ok {\n\t\t\t\tuserEmails = append(userEmails, email)\n\t\t\t}\n\t\t}\n\n\t\tsysConfig := systemConfig{\n\t\t\tName:  system.GetString(\"name\"),\n\t\t\tHost:  system.GetString(\"host\"),\n\t\t\tPort:  cast.ToUint16(system.Get(\"port\")),\n\t\t\tUsers: userEmails,\n\t\t\tToken: systemTokenMap[system.Id],\n\t\t}\n\t\tconfig.Systems = append(config.Systems, sysConfig)\n\t}\n\n\t// Marshal the Config struct to YAML\n\tyamlData, err := yaml.Marshal(&config)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Add a header to the YAML\n\tyamlData = append([]byte(\"# Values for port, users, and token are optional.\\n# Defaults are port 45876, the first created user, and a generated UUID token.\\n\\n\"), yamlData...)\n\n\treturn string(yamlData), nil\n}\n\n// New helper function to get a map of user IDs to emails\nfunc getUserEmailMap(h core.App, userIDs []string) (map[string]string, error) {\n\tusers, err := h.FindRecordsByIds(\"users\", userIDs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserEmailMap := make(map[string]string, len(users))\n\tfor _, user := range users {\n\t\tuserEmailMap[user.Id] = user.GetString(\"email\")\n\t}\n\n\treturn userEmailMap, nil\n}\n\n// Helper function to update or create fingerprint token for an existing system\nfunc updateFingerprintToken(app core.App, systemID, token string) error {\n\t// Try to find existing fingerprint record\n\tfingerprint, err := app.FindFirstRecordByFilter(\"fingerprints\", \"system = {:system}\", dbx.Params{\"system\": systemID})\n\tif err != nil {\n\t\t// If no fingerprint record exists, create one\n\t\treturn createFingerprintRecord(app, systemID, token)\n\t}\n\n\t// Update existing fingerprint record with new token (keep existing fingerprint)\n\tfingerprint.Set(\"token\", token)\n\treturn app.Save(fingerprint)\n}\n\n// Helper function to create a new fingerprint record for a system\nfunc createFingerprintRecord(app core.App, systemID, token string) error {\n\tfingerprintsCollection, err := app.FindCollectionByNameOrId(\"fingerprints\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find fingerprints collection: %v\", err)\n\t}\n\n\tnewFingerprint := core.NewRecord(fingerprintsCollection)\n\tnewFingerprint.Set(\"system\", systemID)\n\tnewFingerprint.Set(\"token\", token)\n\tnewFingerprint.Set(\"fingerprint\", \"\") // Empty fingerprint, will be set on first connection\n\n\treturn app.Save(newFingerprint)\n}\n\n// Returns the current config.yml file as a JSON object\nfunc GetYamlConfig(e *core.RequestEvent) error {\n\tif e.Auth.GetString(\"role\") != \"admin\" {\n\t\treturn e.ForbiddenError(\"Requires admin role\", nil)\n\t}\n\tconfigContent, err := generateYAML(e.App)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn e.JSON(200, map[string]string{\"config\": configContent})\n}\n"
  },
  {
    "path": "internal/hub/config/config_test.go",
    "content": "//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/internal/tests\"\n\n\t\"github.com/henrygd/beszel/internal/hub/config\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Config struct for testing (copied from config package since it's not exported)\ntype testConfig struct {\n\tSystems []testSystemConfig `yaml:\"systems\"`\n}\n\ntype testSystemConfig struct {\n\tName  string   `yaml:\"name\"`\n\tHost  string   `yaml:\"host\"`\n\tPort  uint16   `yaml:\"port,omitempty\"`\n\tUsers []string `yaml:\"users\"`\n\tToken string   `yaml:\"token,omitempty\"`\n}\n\n// Helper function to create a test system for config tests\n// func createConfigTestSystem(app core.App, name, host string, port uint16, userIDs []string) (*core.Record, error) {\n// \tsystemCollection, err := app.FindCollectionByNameOrId(\"systems\")\n// \tif err != nil {\n// \t\treturn nil, err\n// \t}\n\n// \tsystem := core.NewRecord(systemCollection)\n// \tsystem.Set(\"name\", name)\n// \tsystem.Set(\"host\", host)\n// \tsystem.Set(\"port\", port)\n// \tsystem.Set(\"users\", userIDs)\n// \tsystem.Set(\"status\", \"pending\")\n\n// \treturn system, app.Save(system)\n// }\n\n// Helper function to create a fingerprint record\nfunc createConfigTestFingerprint(app core.App, systemID, token, fingerprint string) (*core.Record, error) {\n\tfingerprintCollection, err := app.FindCollectionByNameOrId(\"fingerprints\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfp := core.NewRecord(fingerprintCollection)\n\tfp.Set(\"system\", systemID)\n\tfp.Set(\"token\", token)\n\tfp.Set(\"fingerprint\", fingerprint)\n\n\treturn fp, app.Save(fp)\n}\n\n// TestConfigSyncWithTokens tests the config.SyncSystems function with various token scenarios\nfunc TestConfigSyncWithTokens(t *testing.T) {\n\ttestHub, err := tests.NewTestHub()\n\trequire.NoError(t, err)\n\tdefer testHub.Cleanup()\n\n\t// Create test user\n\tuser, err := tests.CreateUser(testHub.App, \"admin@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tsetupFunc   func() (string, *core.Record, *core.Record) // Returns: existing token, system record, fingerprint record\n\t\tconfigYAML  string\n\t\texpectToken string // Expected token after sync\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tname: \"new system with token in config\",\n\t\t\tsetupFunc: func() (string, *core.Record, *core.Record) {\n\t\t\t\treturn \"\", nil, nil // No existing system\n\t\t\t},\n\t\t\tconfigYAML: `systems:\n  - name: \"new-server\"\n    host: \"new.example.com\"\n    port: 45876\n    users:\n      - \"admin@example.com\"\n    token: \"explicit-token-123\"`,\n\t\t\texpectToken: \"explicit-token-123\",\n\t\t\tdescription: \"New system should use token from config\",\n\t\t},\n\t\t{\n\t\t\tname: \"existing system without token in config (preserve existing)\",\n\t\t\tsetupFunc: func() (string, *core.Record, *core.Record) {\n\t\t\t\t// Create existing system and fingerprint\n\t\t\t\tsystem, err := tests.CreateRecord(testHub.App, \"systems\", map[string]any{\n\t\t\t\t\t\"name\":  \"preserve-server\",\n\t\t\t\t\t\"host\":  \"preserve.example.com\",\n\t\t\t\t\t\"port\":  45876,\n\t\t\t\t\t\"users\": []string{user.Id},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfingerprint, err := createConfigTestFingerprint(testHub.App, system.Id, \"preserve-token-999\", \"preserve-fingerprint\")\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn \"preserve-token-999\", system, fingerprint\n\t\t\t},\n\t\t\tconfigYAML: `systems:\n  - name: \"preserve-server\"\n    host: \"preserve.example.com\"\n    port: 45876\n    users:\n      - \"admin@example.com\"`,\n\t\t\texpectToken: \"preserve-token-999\",\n\t\t\tdescription: \"Existing system should preserve original token when config doesn't specify one\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Setup test data\n\t\t\t_, existingSystem, existingFingerprint := tc.setupFunc()\n\n\t\t\t// Write config file\n\t\t\tconfigPath := filepath.Join(testHub.DataDir(), \"config.yml\")\n\t\t\terr := os.WriteFile(configPath, []byte(tc.configYAML), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create serve event and sync\n\t\t\tevent := &core.ServeEvent{App: testHub.App}\n\t\t\terr = config.SyncSystems(event)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Parse the config to get the system name for verification\n\t\t\tvar configData testConfig\n\t\t\terr = yaml.Unmarshal([]byte(tc.configYAML), &configData)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, configData.Systems, 1)\n\t\t\tsystemName := configData.Systems[0].Name\n\n\t\t\t// Find the system after sync\n\t\t\tsystems, err := testHub.FindRecordsByFilter(\"systems\", \"name = {:name}\", \"\", -1, 0, map[string]any{\"name\": systemName})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, systems, 1)\n\t\t\tsystem := systems[0]\n\n\t\t\t// Find the fingerprint record\n\t\t\tfingerprints, err := testHub.FindRecordsByFilter(\"fingerprints\", \"system = {:system}\", \"\", -1, 0, map[string]any{\"system\": system.Id})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, fingerprints, 1)\n\t\t\tfingerprint := fingerprints[0]\n\n\t\t\t// Verify token\n\t\t\tactualToken := fingerprint.GetString(\"token\")\n\t\t\tif tc.expectToken == \"\" {\n\t\t\t\t// For generated tokens, just verify it's not empty and is a valid UUID format\n\t\t\t\tassert.NotEmpty(t, actualToken, tc.description)\n\t\t\t\tassert.Len(t, actualToken, 36, \"Generated token should be UUID format\") // UUID length\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expectToken, actualToken, tc.description)\n\t\t\t}\n\n\t\t\t// For existing systems, verify fingerprint is preserved\n\t\t\tif existingFingerprint != nil {\n\t\t\t\tactualFingerprint := fingerprint.GetString(\"fingerprint\")\n\t\t\t\texpectedFingerprint := existingFingerprint.GetString(\"fingerprint\")\n\t\t\t\tassert.Equal(t, expectedFingerprint, actualFingerprint, \"Fingerprint should be preserved\")\n\t\t\t}\n\n\t\t\t// Cleanup for next test\n\t\t\tif existingSystem != nil {\n\t\t\t\ttestHub.Delete(existingSystem)\n\t\t\t}\n\t\t\tif existingFingerprint != nil {\n\t\t\t\ttestHub.Delete(existingFingerprint)\n\t\t\t}\n\t\t\t// Clean up the new records\n\t\t\ttestHub.Delete(system)\n\t\t\ttestHub.Delete(fingerprint)\n\t\t})\n\t}\n}\n\n// TestConfigMigrationScenario tests the specific migration scenario mentioned in the discussion\nfunc TestConfigMigrationScenario(t *testing.T) {\n\ttestHub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer testHub.Cleanup()\n\n\t// Create test user\n\tuser, err := tests.CreateUser(testHub.App, \"admin@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\t// Simulate migration scenario: system exists with token from migration\n\texistingSystem, err := tests.CreateRecord(testHub.App, \"systems\", map[string]any{\n\t\t\"name\":  \"migrated-server\",\n\t\t\"host\":  \"migrated.example.com\",\n\t\t\"port\":  45876,\n\t\t\"users\": []string{user.Id},\n\t})\n\trequire.NoError(t, err)\n\n\tmigrationToken := \"migration-generated-token-123\"\n\texistingFingerprint, err := createConfigTestFingerprint(testHub.App, existingSystem.Id, migrationToken, \"existing-fingerprint-from-agent\")\n\trequire.NoError(t, err)\n\n\t// User exports config BEFORE this update (so no token field in YAML)\n\toldConfigYAML := `systems:\n  - name: \"migrated-server\"\n    host: \"migrated.example.com\"\n    port: 45876\n    users:\n      - \"admin@example.com\"`\n\n\t// Write old config file and import\n\tconfigPath := filepath.Join(testHub.DataDir(), \"config.yml\")\n\terr = os.WriteFile(configPath, []byte(oldConfigYAML), 0644)\n\trequire.NoError(t, err)\n\n\tevent := &core.ServeEvent{App: testHub.App}\n\terr = config.SyncSystems(event)\n\trequire.NoError(t, err)\n\n\t// Verify the original token is preserved\n\tupdatedFingerprint, err := testHub.FindRecordById(\"fingerprints\", existingFingerprint.Id)\n\trequire.NoError(t, err)\n\n\tactualToken := updatedFingerprint.GetString(\"token\")\n\tassert.Equal(t, migrationToken, actualToken, \"Migration token should be preserved when config doesn't specify a token\")\n\n\t// Verify fingerprint is also preserved\n\tactualFingerprint := updatedFingerprint.GetString(\"fingerprint\")\n\tassert.Equal(t, \"existing-fingerprint-from-agent\", actualFingerprint, \"Existing fingerprint should be preserved\")\n\n\t// Verify system still exists and is updated correctly\n\tupdatedSystem, err := testHub.FindRecordById(\"systems\", existingSystem.Id)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"migrated-server\", updatedSystem.GetString(\"name\"))\n\tassert.Equal(t, \"migrated.example.com\", updatedSystem.GetString(\"host\"))\n}\n"
  },
  {
    "path": "internal/hub/expirymap/expirymap.go",
    "content": "// Package expirymap provides a thread-safe map with expiring entries.\n// It supports TTL-based expiration with both lazy cleanup on access\n// and periodic background cleanup.\npackage expirymap\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/tools/store\"\n)\n\ntype val[T comparable] struct {\n\tvalue   T\n\texpires time.Time\n}\n\ntype ExpiryMap[T comparable] struct {\n\tstore    *store.Store[string, val[T]]\n\tstopChan chan struct{}\n\tstopOnce sync.Once\n}\n\n// New creates a new expiry map with custom cleanup interval\nfunc New[T comparable](cleanupInterval time.Duration) *ExpiryMap[T] {\n\tm := &ExpiryMap[T]{\n\t\tstore:    store.New(map[string]val[T]{}),\n\t\tstopChan: make(chan struct{}),\n\t}\n\tgo m.startCleaner(cleanupInterval)\n\treturn m\n}\n\n// Set stores a value with the given TTL\nfunc (m *ExpiryMap[T]) Set(key string, value T, ttl time.Duration) {\n\tm.store.Set(key, val[T]{\n\t\tvalue:   value,\n\t\texpires: time.Now().Add(ttl),\n\t})\n}\n\n// GetOk retrieves a value and checks if it exists and hasn't expired\n// Performs lazy cleanup of expired entries on access\nfunc (m *ExpiryMap[T]) GetOk(key string) (T, bool) {\n\tvalue, ok := m.store.GetOk(key)\n\tif !ok {\n\t\treturn *new(T), false\n\t}\n\n\t// Check if expired and perform lazy cleanup\n\tif value.expires.Before(time.Now()) {\n\t\tm.store.Remove(key)\n\t\treturn *new(T), false\n\t}\n\n\treturn value.value, true\n}\n\n// GetByValue retrieves a value by value\nfunc (m *ExpiryMap[T]) GetByValue(val T) (key string, value T, ok bool) {\n\tfor key, v := range m.store.GetAll() {\n\t\tif v.value == val {\n\t\t\t// check if expired\n\t\t\tif v.expires.Before(time.Now()) {\n\t\t\t\tm.store.Remove(key)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn key, v.value, true\n\t\t}\n\t}\n\treturn \"\", *new(T), false\n}\n\n// Remove explicitly removes a key\nfunc (m *ExpiryMap[T]) Remove(key string) {\n\tm.store.Remove(key)\n}\n\n// RemovebyValue removes a value by value\nfunc (m *ExpiryMap[T]) RemovebyValue(value T) (T, bool) {\n\tfor key, val := range m.store.GetAll() {\n\t\tif val.value == value {\n\t\t\tm.store.Remove(key)\n\t\t\treturn val.value, true\n\t\t}\n\t}\n\treturn *new(T), false\n}\n\n// startCleaner runs the background cleanup process\nfunc (m *ExpiryMap[T]) startCleaner(interval time.Duration) {\n\ttick := time.Tick(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-tick:\n\t\t\tm.cleanup()\n\t\tcase <-m.stopChan:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// StopCleaner stops the background cleanup process\nfunc (m *ExpiryMap[T]) StopCleaner() {\n\tm.stopOnce.Do(func() {\n\t\tclose(m.stopChan)\n\t})\n}\n\n// cleanup removes all expired entries\nfunc (m *ExpiryMap[T]) cleanup() {\n\tnow := time.Now()\n\tfor key, val := range m.store.GetAll() {\n\t\tif val.expires.Before(now) {\n\t\t\tm.store.Remove(key)\n\t\t}\n\t}\n}\n\n// UpdateExpiration updates the expiration time of a key\nfunc (m *ExpiryMap[T]) UpdateExpiration(key string, ttl time.Duration) {\n\tvalue, ok := m.store.GetOk(key)\n\tif ok {\n\t\tvalue.expires = time.Now().Add(ttl)\n\t\tm.store.Set(key, value)\n\t}\n}\n"
  },
  {
    "path": "internal/hub/expirymap/expirymap_test.go",
    "content": "//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/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Not using the following methods but are useful for testing\n\n// TESTING: Has checks if a key exists and hasn't expired\nfunc (m *ExpiryMap[T]) Has(key string) bool {\n\t_, ok := m.GetOk(key)\n\treturn ok\n}\n\n// TESTING: Get retrieves a value, returns zero value if not found or expired\nfunc (m *ExpiryMap[T]) Get(key string) T {\n\tvalue, _ := m.GetOk(key)\n\treturn value\n}\n\n// TESTING: Len returns the number of non-expired entries\nfunc (m *ExpiryMap[T]) Len() int {\n\tcount := 0\n\tnow := time.Now()\n\tfor _, val := range m.store.Values() {\n\t\tif val.expires.After(now) {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\nfunc TestExpiryMap_BasicOperations(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Test Set and GetOk\n\tem.Set(\"key1\", \"value1\", time.Hour)\n\tvalue, ok := em.GetOk(\"key1\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value1\", value)\n\n\t// Test Get\n\tvalue = em.Get(\"key1\")\n\tassert.Equal(t, \"value1\", value)\n\n\t// Test Has\n\tassert.True(t, em.Has(\"key1\"))\n\tassert.False(t, em.Has(\"nonexistent\"))\n\n\t// Test Remove\n\tem.Remove(\"key1\")\n\tassert.False(t, em.Has(\"key1\"))\n}\n\nfunc TestExpiryMap_Expiration(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Set a value with very short TTL\n\tem.Set(\"shortlived\", \"value\", time.Millisecond*10)\n\n\t// Should exist immediately\n\tassert.True(t, em.Has(\"shortlived\"))\n\n\t// Wait for expiration\n\ttime.Sleep(time.Millisecond * 20)\n\n\t// Should be expired and automatically cleaned up on access\n\tassert.False(t, em.Has(\"shortlived\"))\n\tvalue, ok := em.GetOk(\"shortlived\")\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", value) // zero value for string\n}\n\nfunc TestExpiryMap_LazyCleanup(t *testing.T) {\n\tem := New[int](time.Hour)\n\n\t// Set multiple values with short TTL\n\tem.Set(\"key1\", 1, time.Millisecond*10)\n\tem.Set(\"key2\", 2, time.Millisecond*10)\n\tem.Set(\"key3\", 3, time.Hour) // This one won't expire\n\n\t// Wait for expiration\n\ttime.Sleep(time.Millisecond * 20)\n\n\t// Access expired keys should trigger lazy cleanup\n\t_, ok := em.GetOk(\"key1\")\n\tassert.False(t, ok)\n\n\t// Non-expired key should still exist\n\tvalue, ok := em.GetOk(\"key3\")\n\tassert.True(t, ok)\n\tassert.Equal(t, 3, value)\n}\n\nfunc TestExpiryMap_Len(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Initially empty\n\tassert.Equal(t, 0, em.Len())\n\n\t// Add some values\n\tem.Set(\"key1\", \"value1\", time.Hour)\n\tem.Set(\"key2\", \"value2\", time.Hour)\n\tem.Set(\"key3\", \"value3\", time.Millisecond*10) // Will expire soon\n\n\t// Should count all initially\n\tassert.Equal(t, 3, em.Len())\n\n\t// Wait for one to expire\n\ttime.Sleep(time.Millisecond * 20)\n\n\t// Len should reflect only non-expired entries\n\tassert.Equal(t, 2, em.Len())\n}\n\nfunc TestExpiryMap_CustomInterval(t *testing.T) {\n\t// Create with very short cleanup interval for testing\n\tem := New[string](time.Millisecond * 50)\n\n\t// Set a value that expires quickly\n\tem.Set(\"test\", \"value\", time.Millisecond*10)\n\n\t// Should exist initially\n\tassert.True(t, em.Has(\"test\"))\n\n\t// Wait for expiration + cleanup cycle\n\ttime.Sleep(time.Millisecond * 100)\n\n\t// Should be cleaned up by background process\n\t// Note: This test might be flaky due to timing, but demonstrates the concept\n\tassert.False(t, em.Has(\"test\"))\n}\n\nfunc TestExpiryMap_GenericTypes(t *testing.T) {\n\t// Test with different types\n\tt.Run(\"Int\", func(t *testing.T) {\n\t\tem := New[int](time.Hour)\n\n\t\tem.Set(\"num\", 42, time.Hour)\n\t\tvalue, ok := em.GetOk(\"num\")\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, 42, value)\n\t})\n\n\tt.Run(\"Struct\", func(t *testing.T) {\n\t\ttype TestStruct struct {\n\t\t\tName string\n\t\t\tAge  int\n\t\t}\n\n\t\tem := New[TestStruct](time.Hour)\n\n\t\texpected := TestStruct{Name: \"John\", Age: 30}\n\t\tem.Set(\"person\", expected, time.Hour)\n\n\t\tvalue, ok := em.GetOk(\"person\")\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, expected, value)\n\t})\n\n\tt.Run(\"Pointer\", func(t *testing.T) {\n\t\tem := New[*string](time.Hour)\n\n\t\tstr := \"hello\"\n\t\tem.Set(\"ptr\", &str, time.Hour)\n\n\t\tvalue, ok := em.GetOk(\"ptr\")\n\t\tassert.True(t, ok)\n\t\trequire.NotNil(t, value)\n\t\tassert.Equal(t, \"hello\", *value)\n\t})\n}\n\nfunc TestExpiryMap_UpdateExpiration(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Set a value with short TTL\n\tem.Set(\"key1\", \"value1\", time.Millisecond*50)\n\n\t// Verify it exists\n\tassert.True(t, em.Has(\"key1\"))\n\n\t// Update expiration to a longer TTL\n\tem.UpdateExpiration(\"key1\", time.Hour)\n\n\t// Wait for the original TTL to pass\n\ttime.Sleep(time.Millisecond * 100)\n\n\t// Should still exist because expiration was updated\n\tassert.True(t, em.Has(\"key1\"))\n\tvalue, ok := em.GetOk(\"key1\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value1\", value)\n\n\t// Try updating non-existent key (should not panic)\n\tassert.NotPanics(t, func() {\n\t\tem.UpdateExpiration(\"nonexistent\", time.Hour)\n\t})\n}\n\nfunc TestExpiryMap_ZeroValues(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Test getting non-existent key returns zero value\n\tvalue := em.Get(\"nonexistent\")\n\tassert.Equal(t, \"\", value)\n\n\t// Test getting expired key returns zero value\n\tem.Set(\"expired\", \"value\", time.Millisecond*10)\n\ttime.Sleep(time.Millisecond * 20)\n\n\tvalue = em.Get(\"expired\")\n\tassert.Equal(t, \"\", value)\n}\n\nfunc TestExpiryMap_Concurrent(t *testing.T) {\n\tem := New[int](time.Hour)\n\n\t// Simple concurrent access test\n\tdone := make(chan bool, 2)\n\n\t// Writer goroutine\n\tgo func() {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tem.Set(\"key\", i, time.Hour)\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t}\n\t\tdone <- true\n\t}()\n\n\t// Reader goroutine\n\tgo func() {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\t_ = em.Get(\"key\")\n\t\t\ttime.Sleep(time.Microsecond)\n\t\t}\n\t\tdone <- true\n\t}()\n\n\t// Wait for both to complete\n\t<-done\n\t<-done\n\n\t// Should not panic and should have some value\n\tassert.True(t, em.Has(\"key\"))\n}\n\nfunc TestExpiryMap_GetByValue(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Test getting by value when value exists\n\tem.Set(\"key1\", \"value1\", time.Hour)\n\tem.Set(\"key2\", \"value2\", time.Hour)\n\tem.Set(\"key3\", \"value1\", time.Hour) // Duplicate value - should return first match\n\n\t// Test successful retrieval\n\tkey, value, ok := em.GetByValue(\"value1\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value1\", value)\n\tassert.Contains(t, []string{\"key1\", \"key3\"}, key) // Should be one of the keys with this value\n\n\t// Test retrieval of unique value\n\tkey, value, ok = em.GetByValue(\"value2\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value2\", value)\n\tassert.Equal(t, \"key2\", key)\n\n\t// Test getting non-existent value\n\tkey, value, ok = em.GetByValue(\"nonexistent\")\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", value) // zero value for string\n\tassert.Equal(t, \"\", key)   // zero value for string\n}\n\nfunc TestExpiryMap_GetByValue_Expiration(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Set a value with short TTL\n\tem.Set(\"shortkey\", \"shortvalue\", time.Millisecond*10)\n\tem.Set(\"longkey\", \"longvalue\", time.Hour)\n\n\t// Should find the short-lived value initially\n\tkey, value, ok := em.GetByValue(\"shortvalue\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shortvalue\", value)\n\tassert.Equal(t, \"shortkey\", key)\n\n\t// Wait for expiration\n\ttime.Sleep(time.Millisecond * 20)\n\n\t// Should not find expired value and should trigger lazy cleanup\n\tkey, value, ok = em.GetByValue(\"shortvalue\")\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", value)\n\tassert.Equal(t, \"\", key)\n\n\t// Should still find non-expired value\n\tkey, value, ok = em.GetByValue(\"longvalue\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"longvalue\", value)\n\tassert.Equal(t, \"longkey\", key)\n}\n\nfunc TestExpiryMap_GetByValue_GenericTypes(t *testing.T) {\n\tt.Run(\"Int\", func(t *testing.T) {\n\t\tem := New[int](time.Hour)\n\n\t\tem.Set(\"num1\", 42, time.Hour)\n\t\tem.Set(\"num2\", 84, time.Hour)\n\n\t\tkey, value, ok := em.GetByValue(42)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, 42, value)\n\t\tassert.Equal(t, \"num1\", key)\n\n\t\tkey, value, ok = em.GetByValue(99)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, 0, value)\n\t\tassert.Equal(t, \"\", key)\n\t})\n\n\tt.Run(\"Struct\", func(t *testing.T) {\n\t\ttype TestStruct struct {\n\t\t\tName string\n\t\t\tAge  int\n\t\t}\n\n\t\tem := New[TestStruct](time.Hour)\n\n\t\tperson1 := TestStruct{Name: \"John\", Age: 30}\n\t\tperson2 := TestStruct{Name: \"Jane\", Age: 25}\n\n\t\tem.Set(\"person1\", person1, time.Hour)\n\t\tem.Set(\"person2\", person2, time.Hour)\n\n\t\tkey, value, ok := em.GetByValue(person1)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, person1, value)\n\t\tassert.Equal(t, \"person1\", key)\n\n\t\tnonexistent := TestStruct{Name: \"Bob\", Age: 40}\n\t\tkey, value, ok = em.GetByValue(nonexistent)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, TestStruct{}, value)\n\t\tassert.Equal(t, \"\", key)\n\t})\n}\n\nfunc TestExpiryMap_RemoveValue(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Test removing existing value\n\tem.Set(\"key1\", \"value1\", time.Hour)\n\tem.Set(\"key2\", \"value2\", time.Hour)\n\tem.Set(\"key3\", \"value1\", time.Hour) // Duplicate value\n\n\t// Remove by value should remove one instance\n\tremovedValue, ok := em.RemovebyValue(\"value1\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value1\", removedValue)\n\n\t// Should still have the other instance or value2\n\tassert.True(t, em.Has(\"key2\")) // value2 should still exist\n\n\t// Check if one of the duplicate values was removed\n\t// At least one key with \"value1\" should be gone\n\tkey1Exists := em.Has(\"key1\")\n\tkey3Exists := em.Has(\"key3\")\n\tassert.False(t, key1Exists && key3Exists) // Both shouldn't exist\n\tassert.True(t, key1Exists || key3Exists)  // At least one should be gone\n\n\t// Test removing non-existent value\n\tremovedValue, ok = em.RemovebyValue(\"nonexistent\")\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", removedValue) // zero value for string\n}\n\nfunc TestExpiryMap_RemoveValue_GenericTypes(t *testing.T) {\n\tt.Run(\"Int\", func(t *testing.T) {\n\t\tem := New[int](time.Hour)\n\n\t\tem.Set(\"num1\", 42, time.Hour)\n\t\tem.Set(\"num2\", 84, time.Hour)\n\n\t\t// Remove existing value\n\t\tremovedValue, ok := em.RemovebyValue(42)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, 42, removedValue)\n\t\tassert.False(t, em.Has(\"num1\"))\n\t\tassert.True(t, em.Has(\"num2\"))\n\n\t\t// Remove non-existent value\n\t\tremovedValue, ok = em.RemovebyValue(99)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, 0, removedValue)\n\t})\n\n\tt.Run(\"Struct\", func(t *testing.T) {\n\t\ttype TestStruct struct {\n\t\t\tName string\n\t\t\tAge  int\n\t\t}\n\n\t\tem := New[TestStruct](time.Hour)\n\n\t\tperson1 := TestStruct{Name: \"John\", Age: 30}\n\t\tperson2 := TestStruct{Name: \"Jane\", Age: 25}\n\n\t\tem.Set(\"person1\", person1, time.Hour)\n\t\tem.Set(\"person2\", person2, time.Hour)\n\n\t\t// Remove existing struct\n\t\tremovedValue, ok := em.RemovebyValue(person1)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, person1, removedValue)\n\t\tassert.False(t, em.Has(\"person1\"))\n\t\tassert.True(t, em.Has(\"person2\"))\n\n\t\t// Remove non-existent struct\n\t\tnonexistent := TestStruct{Name: \"Bob\", Age: 40}\n\t\tremovedValue, ok = em.RemovebyValue(nonexistent)\n\t\tassert.False(t, ok)\n\t\tassert.Equal(t, TestStruct{}, removedValue)\n\t})\n}\n\nfunc TestExpiryMap_RemoveValue_WithExpiration(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Set values with different TTLs\n\tem.Set(\"key1\", \"value1\", time.Millisecond*10) // Will expire\n\tem.Set(\"key2\", \"value2\", time.Hour)           // Won't expire\n\tem.Set(\"key3\", \"value1\", time.Hour)           // Won't expire, duplicate value\n\n\t// Wait for first value to expire\n\ttime.Sleep(time.Millisecond * 20)\n\n\t// Trigger lazy cleanup of the expired key\n\t_, ok := em.GetOk(\"key1\")\n\tassert.False(t, ok)\n\n\t// Try to remove the remaining \"value1\" entry (key3)\n\tremovedValue, ok := em.RemovebyValue(\"value1\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"value1\", removedValue)\n\n\t// Should still have key2 (different value)\n\tassert.True(t, em.Has(\"key2\"))\n\n\t// key1 should be gone due to expiration and key3 should be removed by value.\n\tassert.False(t, em.Has(\"key1\"))\n\tassert.False(t, em.Has(\"key3\"))\n}\n\nfunc TestExpiryMap_ValueOperations_Integration(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Test integration of GetByValue and RemoveValue\n\tem.Set(\"key1\", \"shared\", time.Hour)\n\tem.Set(\"key2\", \"unique\", time.Hour)\n\tem.Set(\"key3\", \"shared\", time.Hour)\n\n\t// Find shared value\n\tkey, value, ok := em.GetByValue(\"shared\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shared\", value)\n\tassert.Contains(t, []string{\"key1\", \"key3\"}, key)\n\n\t// Remove shared value\n\tremovedValue, ok := em.RemovebyValue(\"shared\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shared\", removedValue)\n\n\t// Should still be able to find the other shared value\n\tkey, value, ok = em.GetByValue(\"shared\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shared\", value)\n\tassert.Contains(t, []string{\"key1\", \"key3\"}, key)\n\n\t// Remove the other shared value\n\tremovedValue, ok = em.RemovebyValue(\"shared\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shared\", removedValue)\n\n\t// Should not find shared value anymore\n\tkey, value, ok = em.GetByValue(\"shared\")\n\tassert.False(t, ok)\n\tassert.Equal(t, \"\", value)\n\tassert.Equal(t, \"\", key)\n\n\t// Unique value should still exist\n\tkey, value, ok = em.GetByValue(\"unique\")\n\tassert.True(t, ok)\n\tassert.Equal(t, \"unique\", value)\n\tassert.Equal(t, \"key2\", key)\n}\n\nfunc TestExpiryMap_Cleaner(t *testing.T) {\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tem := New[string](time.Second)\n\t\tdefer em.StopCleaner()\n\n\t\tem.Set(\"test\", \"value\", 500*time.Millisecond)\n\n\t\t// Wait 600ms, value is expired but cleaner hasn't run yet (interval is 1s)\n\t\ttime.Sleep(600 * time.Millisecond)\n\t\tsynctest.Wait()\n\n\t\t// Map should still hold the value in its internal store before lazy access or cleaner\n\t\tassert.Equal(t, 1, len(em.store.GetAll()), \"store should still have 1 item before cleaner runs\")\n\n\t\t// Wait another 500ms so cleaner (1s interval) runs\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tsynctest.Wait() // Wait for background goroutine to process the tick\n\n\t\tassert.Equal(t, 0, len(em.store.GetAll()), \"store should be empty after cleaner runs\")\n\t})\n}\n\nfunc TestExpiryMap_StopCleaner(t *testing.T) {\n\tem := New[string](time.Hour)\n\n\t// Initially, stopChan is open, reading would block\n\tselect {\n\tcase <-em.stopChan:\n\t\tt.Fatal(\"stopChan should be open initially\")\n\tdefault:\n\t\t// success\n\t}\n\n\tem.StopCleaner()\n\n\t// After StopCleaner, stopChan is closed, reading returns immediately\n\tselect {\n\tcase <-em.stopChan:\n\t\t// success\n\tdefault:\n\t\tt.Fatal(\"stopChan was not closed by StopCleaner\")\n\t}\n\n\t// Calling StopCleaner again should NOT panic thanks to sync.Once\n\tassert.NotPanics(t, func() {\n\t\tem.StopCleaner()\n\t})\n}\n"
  },
  {
    "path": "internal/hub/heartbeat/heartbeat.go",
    "content": "// Package heartbeat sends periodic outbound pings to an external monitoring\n// endpoint (e.g. BetterStack, Uptime Kuma, Healthchecks.io) so operators can\n// monitor Beszel without exposing it to the internet.\npackage heartbeat\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// Default values for heartbeat configuration.\nconst (\n\tdefaultInterval = 60 // seconds\n\thttpTimeout     = 10 * time.Second\n)\n\n// Payload is the JSON body sent with each heartbeat request.\ntype Payload struct {\n\t// Status is \"ok\" when all non-paused systems are up, \"warn\" when alerts\n\t// are triggered but no systems are down, and \"error\" when any system is down.\n\tStatus    string         `json:\"status\"`\n\tTimestamp string         `json:\"timestamp\"`\n\tMsg       string         `json:\"msg\"`\n\tSystems   SystemsSummary `json:\"systems\"`\n\tDown      []SystemInfo   `json:\"down_systems,omitempty\"`\n\tAlerts    []AlertInfo    `json:\"triggered_alerts,omitempty\"`\n\tVersion   string         `json:\"beszel_version\"`\n}\n\n// SystemsSummary contains counts of systems by status.\ntype SystemsSummary struct {\n\tTotal   int `json:\"total\"`\n\tUp      int `json:\"up\"`\n\tDown    int `json:\"down\"`\n\tPaused  int `json:\"paused\"`\n\tPending int `json:\"pending\"`\n}\n\n// SystemInfo identifies a system that is currently down.\ntype SystemInfo struct {\n\tID   string `json:\"id\" db:\"id\"`\n\tName string `json:\"name\" db:\"name\"`\n\tHost string `json:\"host\" db:\"host\"`\n}\n\n// AlertInfo describes a currently triggered alert.\ntype AlertInfo struct {\n\tSystemID   string  `json:\"system_id\"`\n\tSystemName string  `json:\"system_name\"`\n\tAlertName  string  `json:\"alert_name\"`\n\tThreshold  float64 `json:\"threshold\"`\n}\n\n// Config holds heartbeat settings read from environment variables.\ntype Config struct {\n\tURL      string // endpoint to ping\n\tInterval int    // seconds between pings\n\tMethod   string // HTTP method (GET or POST, default POST)\n}\n\n// Heartbeat manages the periodic outbound health check.\ntype Heartbeat struct {\n\tapp    core.App\n\tconfig Config\n\tclient *http.Client\n}\n\n// New creates a Heartbeat if configuration is present.\n// Returns nil if HEARTBEAT_URL is not set (feature disabled).\nfunc New(app core.App, getEnv func(string) (string, bool)) *Heartbeat {\n\turl, _ := getEnv(\"HEARTBEAT_URL\")\n\turl = strings.TrimSpace(url)\n\tif app == nil || url == \"\" {\n\t\treturn nil\n\t}\n\n\tinterval := defaultInterval\n\tif v, ok := getEnv(\"HEARTBEAT_INTERVAL\"); ok {\n\t\tif parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {\n\t\t\tinterval = parsed\n\t\t}\n\t}\n\n\tmethod := http.MethodPost\n\tif v, ok := getEnv(\"HEARTBEAT_METHOD\"); ok {\n\t\tv = strings.ToUpper(strings.TrimSpace(v))\n\t\tif v == http.MethodGet || v == http.MethodHead {\n\t\t\tmethod = v\n\t\t}\n\t}\n\n\treturn &Heartbeat{\n\t\tapp: app,\n\t\tconfig: Config{\n\t\t\tURL:      url,\n\t\t\tInterval: interval,\n\t\t\tMethod:   method,\n\t\t},\n\t\tclient: &http.Client{Timeout: httpTimeout},\n\t}\n}\n\n// Start begins the heartbeat loop. It blocks and should be called in a goroutine.\n// The loop runs until the provided stop channel is closed.\nfunc (hb *Heartbeat) Start(stop <-chan struct{}) {\n\tsanitizedURL := sanitizeHeartbeatURL(hb.config.URL)\n\thb.app.Logger().Info(\"Heartbeat enabled\",\n\t\t\"url\", sanitizedURL,\n\t\t\"interval\", fmt.Sprintf(\"%ds\", hb.config.Interval),\n\t\t\"method\", hb.config.Method,\n\t)\n\n\t// Send an initial heartbeat immediately on startup.\n\thb.send()\n\n\tticker := time.NewTicker(time.Duration(hb.config.Interval) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-stop:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\thb.send()\n\t\t}\n\t}\n}\n\n// Send performs a single heartbeat ping. Exposed for the test-heartbeat API endpoint.\nfunc (hb *Heartbeat) Send() error {\n\treturn hb.send()\n}\n\n// GetConfig returns the current heartbeat configuration.\nfunc (hb *Heartbeat) GetConfig() Config {\n\treturn hb.config\n}\n\nfunc (hb *Heartbeat) send() error {\n\tvar req *http.Request\n\tvar err error\n\tmethod := normalizeMethod(hb.config.Method)\n\n\tif method == http.MethodGet || method == http.MethodHead {\n\t\treq, err = http.NewRequest(method, hb.config.URL, nil)\n\t} else {\n\t\tpayload, payloadErr := hb.buildPayload()\n\t\tif payloadErr != nil {\n\t\t\thb.app.Logger().Error(\"Heartbeat: failed to build payload\", \"err\", payloadErr)\n\t\t\treturn payloadErr\n\t\t}\n\n\t\tbody, jsonErr := json.Marshal(payload)\n\t\tif jsonErr != nil {\n\t\t\thb.app.Logger().Error(\"Heartbeat: failed to marshal payload\", \"err\", jsonErr)\n\t\t\treturn jsonErr\n\t\t}\n\t\treq, err = http.NewRequest(http.MethodPost, hb.config.URL, bytes.NewReader(body))\n\t\tif err == nil {\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t}\n\t}\n\n\tif err != nil {\n\t\thb.app.Logger().Error(\"Heartbeat: failed to create request\", \"err\", err)\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"User-Agent\", \"Beszel-Heartbeat\")\n\n\tresp, err := hb.client.Do(req)\n\tif err != nil {\n\t\thb.app.Logger().Error(\"Heartbeat: request failed\", \"url\", sanitizeHeartbeatURL(hb.config.URL), \"err\", err)\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\thb.app.Logger().Warn(\"Heartbeat: non-success response\",\n\t\t\t\"url\", sanitizeHeartbeatURL(hb.config.URL),\n\t\t\t\"status\", resp.StatusCode,\n\t\t)\n\t\treturn fmt.Errorf(\"heartbeat endpoint returned status %d\", resp.StatusCode)\n\t}\n\n\treturn nil\n}\n\nfunc (hb *Heartbeat) buildPayload() (*Payload, error) {\n\tdb := hb.app.DB()\n\n\t// Count systems by status.\n\tvar systemCounts []struct {\n\t\tStatus string `db:\"status\"`\n\t\tCount  int    `db:\"cnt\"`\n\t}\n\terr := db.NewQuery(\"SELECT status, COUNT(*) as cnt FROM systems GROUP BY status\").All(&systemCounts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"query system counts: %w\", err)\n\t}\n\n\tsummary := SystemsSummary{}\n\tfor _, sc := range systemCounts {\n\t\tswitch sc.Status {\n\t\tcase \"up\":\n\t\t\tsummary.Up = sc.Count\n\t\tcase \"down\":\n\t\t\tsummary.Down = sc.Count\n\t\tcase \"paused\":\n\t\t\tsummary.Paused = sc.Count\n\t\tcase \"pending\":\n\t\t\tsummary.Pending = sc.Count\n\t\t}\n\t\tsummary.Total += sc.Count\n\t}\n\n\t// Get names of down systems.\n\tvar downSystems []SystemInfo\n\tif summary.Down > 0 {\n\t\terr = db.NewQuery(\"SELECT id, name, host FROM systems WHERE status = 'down'\").All(&downSystems)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"query down systems: %w\", err)\n\t\t}\n\t}\n\n\t// Get triggered alerts with system names.\n\tvar triggeredAlerts []struct {\n\t\tSystemID   string  `db:\"system\"`\n\t\tSystemName string  `db:\"system_name\"`\n\t\tAlertName  string  `db:\"name\"`\n\t\tValue      float64 `db:\"value\"`\n\t}\n\terr = db.NewQuery(`\n\t\tSELECT a.system, s.name as system_name, a.name, a.value\n\t\tFROM alerts a\n\t\tJOIN systems s ON a.system = s.id\n\t\tWHERE a.triggered = true\n\t`).All(&triggeredAlerts)\n\tif err != nil {\n\t\t// Non-fatal: alerts info is supplementary.\n\t\ttriggeredAlerts = nil\n\t}\n\n\talerts := make([]AlertInfo, 0, len(triggeredAlerts))\n\tfor _, ta := range triggeredAlerts {\n\t\talerts = append(alerts, AlertInfo{\n\t\t\tSystemID:   ta.SystemID,\n\t\t\tSystemName: ta.SystemName,\n\t\t\tAlertName:  ta.AlertName,\n\t\t\tThreshold:  ta.Value,\n\t\t})\n\t}\n\n\t// Determine overall status.\n\tstatus := \"ok\"\n\tmsg := \"All systems operational\"\n\tif summary.Down > 0 {\n\t\tstatus = \"error\"\n\t\tnames := make([]string, len(downSystems))\n\t\tfor i, ds := range downSystems {\n\t\t\tnames[i] = ds.Name\n\t\t}\n\t\tmsg = fmt.Sprintf(\"%d system(s) down: %s\", summary.Down, strings.Join(names, \", \"))\n\t} else if len(alerts) > 0 {\n\t\tstatus = \"warn\"\n\t\tmsg = fmt.Sprintf(\"%d alert(s) triggered\", len(alerts))\n\t}\n\n\treturn &Payload{\n\t\tStatus:    status,\n\t\tTimestamp: time.Now().UTC().Format(time.RFC3339),\n\t\tMsg:       msg,\n\t\tSystems:   summary,\n\t\tDown:      downSystems,\n\t\tAlerts:    alerts,\n\t\tVersion:   beszel.Version,\n\t}, nil\n}\n\nfunc normalizeMethod(method string) string {\n\tupper := strings.ToUpper(strings.TrimSpace(method))\n\tif upper == http.MethodGet || upper == http.MethodHead || upper == http.MethodPost {\n\t\treturn upper\n\t}\n\treturn http.MethodPost\n}\n\nfunc sanitizeHeartbeatURL(rawURL string) string {\n\tparsed, err := url.Parse(strings.TrimSpace(rawURL))\n\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\treturn \"<invalid-url>\"\n\t}\n\treturn parsed.Scheme + \"://\" + parsed.Host\n}\n"
  },
  {
    "path": "internal/hub/heartbeat/heartbeat_test.go",
    "content": "//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\n\t\"github.com/henrygd/beszel/internal/hub/heartbeat\"\n\tbeszeltests \"github.com/henrygd/beszel/internal/tests\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Run(\"returns nil when app is missing\", func(t *testing.T) {\n\t\thb := heartbeat.New(nil, envGetter(map[string]string{\n\t\t\t\"HEARTBEAT_URL\": \"https://heartbeat.example.com/ping\",\n\t\t}))\n\t\tassert.Nil(t, hb)\n\t})\n\n\tt.Run(\"returns nil when URL is missing\", func(t *testing.T) {\n\t\tapp := newTestHub(t)\n\t\thb := heartbeat.New(app.App, func(string) (string, bool) {\n\t\t\treturn \"\", false\n\t\t})\n\t\tassert.Nil(t, hb)\n\t})\n\n\tt.Run(\"parses and normalizes config values\", func(t *testing.T) {\n\t\tapp := newTestHub(t)\n\t\tenv := map[string]string{\n\t\t\t\"HEARTBEAT_URL\":      \"  https://heartbeat.example.com/ping  \",\n\t\t\t\"HEARTBEAT_INTERVAL\": \"90\",\n\t\t\t\"HEARTBEAT_METHOD\":   \"head\",\n\t\t}\n\t\tgetEnv := func(key string) (string, bool) {\n\t\t\tv, ok := env[key]\n\t\t\treturn v, ok\n\t\t}\n\n\t\thb := heartbeat.New(app.App, getEnv)\n\t\trequire.NotNil(t, hb)\n\t\tcfg := hb.GetConfig()\n\t\tassert.Equal(t, \"https://heartbeat.example.com/ping\", cfg.URL)\n\t\tassert.Equal(t, 90, cfg.Interval)\n\t\tassert.Equal(t, http.MethodHead, cfg.Method)\n\t})\n}\n\nfunc TestSendGETDoesNotRequireAppOrDB(t *testing.T) {\n\tapp := newTestHub(t)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tassert.Equal(t, http.MethodGet, r.Method)\n\t\tassert.Equal(t, \"Beszel-Heartbeat\", r.Header.Get(\"User-Agent\"))\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer server.Close()\n\n\thb := heartbeat.New(app.App, envGetter(map[string]string{\n\t\t\"HEARTBEAT_URL\":    server.URL,\n\t\t\"HEARTBEAT_METHOD\": \"GET\",\n\t}))\n\trequire.NotNil(t, hb)\n\n\trequire.NoError(t, hb.Send())\n}\n\nfunc TestSendReturnsErrorOnHTTPFailureStatus(t *testing.T) {\n\tapp := newTestHub(t)\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t}))\n\tdefer server.Close()\n\n\thb := heartbeat.New(app.App, envGetter(map[string]string{\n\t\t\"HEARTBEAT_URL\":    server.URL,\n\t\t\"HEARTBEAT_METHOD\": \"GET\",\n\t}))\n\trequire.NotNil(t, hb)\n\n\terr := hb.Send()\n\trequire.Error(t, err)\n\tassert.ErrorContains(t, err, \"heartbeat endpoint returned status 500\")\n}\n\nfunc TestSendPOSTBuildsExpectedStatuses(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tsetup          func(t *testing.T, app *beszeltests.TestHub, user *core.Record)\n\t\texpectStatus   string\n\t\texpectMsgPart  string\n\t\texpectDown     int\n\t\texpectAlerts   int\n\t\texpectTotal    int\n\t\texpectUp       int\n\t\texpectPaused   int\n\t\texpectPending  int\n\t\texpectDownSumm int\n\t}{\n\t\t{\n\t\t\tname: \"error when at least one system is down\",\n\t\t\tsetup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {\n\t\t\t\tdownSystem := createTestSystem(t, app, user.Id, \"db-1\", \"10.0.0.1\", \"down\")\n\t\t\t\t_ = createTestSystem(t, app, user.Id, \"web-1\", \"10.0.0.2\", \"up\")\n\t\t\t\tcreateTriggeredAlert(t, app, user.Id, downSystem.Id, \"CPU\", 95)\n\t\t\t},\n\t\t\texpectStatus:   \"error\",\n\t\t\texpectMsgPart:  \"1 system(s) down\",\n\t\t\texpectDown:     1,\n\t\t\texpectAlerts:   1,\n\t\t\texpectTotal:    2,\n\t\t\texpectUp:       1,\n\t\t\texpectDownSumm: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"warn when only alerts are triggered\",\n\t\t\tsetup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {\n\t\t\t\tsystem := createTestSystem(t, app, user.Id, \"api-1\", \"10.1.0.1\", \"up\")\n\t\t\t\tcreateTriggeredAlert(t, app, user.Id, system.Id, \"CPU\", 90)\n\t\t\t},\n\t\t\texpectStatus:   \"warn\",\n\t\t\texpectMsgPart:  \"1 alert(s) triggered\",\n\t\t\texpectDown:     0,\n\t\t\texpectAlerts:   1,\n\t\t\texpectTotal:    1,\n\t\t\texpectUp:       1,\n\t\t\texpectDownSumm: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"ok when no down systems and no alerts\",\n\t\t\tsetup: func(t *testing.T, app *beszeltests.TestHub, user *core.Record) {\n\t\t\t\t_ = createTestSystem(t, app, user.Id, \"node-1\", \"10.2.0.1\", \"up\")\n\t\t\t\t_ = createTestSystem(t, app, user.Id, \"node-2\", \"10.2.0.2\", \"paused\")\n\t\t\t\t_ = createTestSystem(t, app, user.Id, \"node-3\", \"10.2.0.3\", \"pending\")\n\t\t\t},\n\t\t\texpectStatus:   \"ok\",\n\t\t\texpectMsgPart:  \"All systems operational\",\n\t\t\texpectDown:     0,\n\t\t\texpectAlerts:   0,\n\t\t\texpectTotal:    3,\n\t\t\texpectUp:       1,\n\t\t\texpectPaused:   1,\n\t\t\texpectPending:  1,\n\t\t\texpectDownSumm: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapp := newTestHub(t)\n\t\t\tuser := createTestUser(t, app)\n\t\t\ttt.setup(t, app, user)\n\n\t\t\ttype requestCapture struct {\n\t\t\t\tmethod      string\n\t\t\t\tuserAgent   string\n\t\t\t\tcontentType string\n\t\t\t\tpayload     heartbeat.Payload\n\t\t\t}\n\n\t\t\tcaptured := make(chan requestCapture, 1)\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tdefer r.Body.Close()\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar payload heartbeat.Payload\n\t\t\t\trequire.NoError(t, json.Unmarshal(body, &payload))\n\t\t\t\tcaptured <- requestCapture{\n\t\t\t\t\tmethod:      r.Method,\n\t\t\t\t\tuserAgent:   r.Header.Get(\"User-Agent\"),\n\t\t\t\t\tcontentType: r.Header.Get(\"Content-Type\"),\n\t\t\t\t\tpayload:     payload,\n\t\t\t\t}\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t}))\n\t\t\tdefer server.Close()\n\n\t\t\thb := heartbeat.New(app.App, envGetter(map[string]string{\n\t\t\t\t\"HEARTBEAT_URL\":    server.URL,\n\t\t\t\t\"HEARTBEAT_METHOD\": \"POST\",\n\t\t\t}))\n\t\t\trequire.NotNil(t, hb)\n\t\t\trequire.NoError(t, hb.Send())\n\n\t\t\treq := <-captured\n\t\t\tassert.Equal(t, http.MethodPost, req.method)\n\t\t\tassert.Equal(t, \"Beszel-Heartbeat\", req.userAgent)\n\t\t\tassert.Equal(t, \"application/json\", req.contentType)\n\n\t\t\tassert.Equal(t, tt.expectStatus, req.payload.Status)\n\t\t\tassert.Contains(t, req.payload.Msg, tt.expectMsgPart)\n\t\t\tassert.Equal(t, tt.expectDown, len(req.payload.Down))\n\t\t\tassert.Equal(t, tt.expectAlerts, len(req.payload.Alerts))\n\t\t\tassert.Equal(t, tt.expectTotal, req.payload.Systems.Total)\n\t\t\tassert.Equal(t, tt.expectUp, req.payload.Systems.Up)\n\t\t\tassert.Equal(t, tt.expectDownSumm, req.payload.Systems.Down)\n\t\t\tassert.Equal(t, tt.expectPaused, req.payload.Systems.Paused)\n\t\t\tassert.Equal(t, tt.expectPending, req.payload.Systems.Pending)\n\t\t})\n\t}\n}\n\nfunc newTestHub(t *testing.T) *beszeltests.TestHub {\n\tt.Helper()\n\tapp, err := beszeltests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tt.Cleanup(app.Cleanup)\n\treturn app\n}\n\nfunc createTestUser(t *testing.T, app *beszeltests.TestHub) *core.Record {\n\tt.Helper()\n\tuser, err := beszeltests.CreateUser(app.App, \"admin@example.com\", \"password123\")\n\trequire.NoError(t, err)\n\treturn user\n}\n\nfunc createTestSystem(t *testing.T, app *beszeltests.TestHub, userID, name, host, status string) *core.Record {\n\tt.Helper()\n\tsystem, err := beszeltests.CreateRecord(app.App, \"systems\", map[string]any{\n\t\t\"name\":   name,\n\t\t\"host\":   host,\n\t\t\"port\":   \"45876\",\n\t\t\"users\":  []string{userID},\n\t\t\"status\": status,\n\t})\n\trequire.NoError(t, err)\n\treturn system\n}\n\nfunc createTriggeredAlert(t *testing.T, app *beszeltests.TestHub, userID, systemID, name string, threshold float64) *core.Record {\n\tt.Helper()\n\talert, err := beszeltests.CreateRecord(app.App, \"alerts\", map[string]any{\n\t\t\"name\":      name,\n\t\t\"system\":    systemID,\n\t\t\"user\":      userID,\n\t\t\"value\":     threshold,\n\t\t\"min\":       0,\n\t\t\"triggered\": true,\n\t})\n\trequire.NoError(t, err)\n\treturn alert\n}\n\nfunc envGetter(values map[string]string) func(string) (string, bool) {\n\treturn func(key string) (string, bool) {\n\t\tv, ok := values[key]\n\t\treturn v, ok\n\t}\n}\n"
  },
  {
    "path": "internal/hub/hub.go",
    "content": "// Package hub handles updating systems and serving the web UI.\npackage hub\n\nimport (\n\t\"crypto/ed25519\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel\"\n\t\"github.com/henrygd/beszel/internal/alerts\"\n\t\"github.com/henrygd/beszel/internal/hub/config\"\n\t\"github.com/henrygd/beszel/internal/hub/heartbeat\"\n\t\"github.com/henrygd/beszel/internal/hub/systems\"\n\t\"github.com/henrygd/beszel/internal/records\"\n\t\"github.com/henrygd/beszel/internal/users\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase\"\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype Hub struct {\n\tcore.App\n\t*alerts.AlertManager\n\tum     *users.UserManager\n\trm     *records.RecordManager\n\tsm     *systems.SystemManager\n\thb     *heartbeat.Heartbeat\n\thbStop chan struct{}\n\tpubKey string\n\tsigner ssh.Signer\n\tappURL string\n}\n\nvar containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)\n\n// NewHub creates a new Hub instance with default configuration\nfunc NewHub(app core.App) *Hub {\n\thub := &Hub{}\n\thub.App = app\n\n\thub.AlertManager = alerts.NewAlertManager(hub)\n\thub.um = users.NewUserManager(hub)\n\thub.rm = records.NewRecordManager(hub)\n\thub.sm = systems.NewSystemManager(hub)\n\thub.appURL, _ = GetEnv(\"APP_URL\")\n\thub.hb = heartbeat.New(app, GetEnv)\n\tif hub.hb != nil {\n\t\thub.hbStop = make(chan struct{})\n\t}\n\treturn hub\n}\n\n// GetEnv retrieves an environment variable with a \"BESZEL_HUB_\" prefix, or falls back to the unprefixed key.\nfunc GetEnv(key string) (value string, exists bool) {\n\tif value, exists = os.LookupEnv(\"BESZEL_HUB_\" + key); exists {\n\t\treturn value, exists\n\t}\n\t// Fallback to the old unprefixed key\n\treturn os.LookupEnv(key)\n}\n\nfunc (h *Hub) StartHub() error {\n\th.App.OnServe().BindFunc(func(e *core.ServeEvent) error {\n\t\t// initialize settings / collections\n\t\tif err := h.initialize(e); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// sync systems with config\n\t\tif err := config.SyncSystems(e); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// register middlewares\n\t\th.registerMiddlewares(e)\n\t\t// register api routes\n\t\tif err := h.registerApiRoutes(e); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// register cron jobs\n\t\tif err := h.registerCronJobs(e); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// start server\n\t\tif err := h.startServer(e); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// start system updates\n\t\tif err := h.sm.Initialize(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// start heartbeat if configured\n\t\tif h.hb != nil {\n\t\t\tgo h.hb.Start(h.hbStop)\n\t\t}\n\t\treturn e.Next()\n\t})\n\n\t// TODO: move to users package\n\t// handle default values for user / user_settings creation\n\th.App.OnRecordCreate(\"users\").BindFunc(h.um.InitializeUserRole)\n\th.App.OnRecordCreate(\"user_settings\").BindFunc(h.um.InitializeUserSettings)\n\n\tif pb, ok := h.App.(*pocketbase.PocketBase); ok {\n\t\t// log.Println(\"Starting pocketbase\")\n\t\terr := pb.Start()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// initialize sets up initial configuration (collections, settings, etc.)\nfunc (h *Hub) initialize(e *core.ServeEvent) error {\n\t// set general settings\n\tsettings := e.App.Settings()\n\t// batch requests (for global alerts)\n\tsettings.Batch.Enabled = true\n\t// set URL if BASE_URL env is set\n\tif h.appURL != \"\" {\n\t\tsettings.Meta.AppURL = h.appURL\n\t}\n\tif err := e.App.Save(settings); err != nil {\n\t\treturn err\n\t}\n\t// set auth settings\n\tif err := setCollectionAuthSettings(e.App); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// setCollectionAuthSettings sets up default authentication settings for the app\nfunc setCollectionAuthSettings(app core.App) error {\n\tusersCollection, err := app.FindCollectionByNameOrId(\"users\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tsuperusersCollection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// disable email auth if DISABLE_PASSWORD_AUTH env var is set\n\tdisablePasswordAuth, _ := GetEnv(\"DISABLE_PASSWORD_AUTH\")\n\tusersCollection.PasswordAuth.Enabled = disablePasswordAuth != \"true\"\n\tusersCollection.PasswordAuth.IdentityFields = []string{\"email\"}\n\t// allow oauth user creation if USER_CREATION is set\n\tif userCreation, _ := GetEnv(\"USER_CREATION\"); userCreation == \"true\" {\n\t\tcr := \"@request.context = 'oauth2'\"\n\t\tusersCollection.CreateRule = &cr\n\t} else {\n\t\tusersCollection.CreateRule = nil\n\t}\n\n\t// enable mfaOtp mfa if MFA_OTP env var is set\n\tmfaOtp, _ := GetEnv(\"MFA_OTP\")\n\tusersCollection.OTP.Length = 6\n\tsuperusersCollection.OTP.Length = 6\n\tusersCollection.OTP.Enabled = mfaOtp == \"true\"\n\tusersCollection.MFA.Enabled = mfaOtp == \"true\"\n\tsuperusersCollection.OTP.Enabled = mfaOtp == \"true\" || mfaOtp == \"superusers\"\n\tsuperusersCollection.MFA.Enabled = mfaOtp == \"true\" || mfaOtp == \"superusers\"\n\tif err := app.Save(superusersCollection); err != nil {\n\t\treturn err\n\t}\n\tif err := app.Save(usersCollection); err != nil {\n\t\treturn err\n\t}\n\n\tshareAllSystems, _ := GetEnv(\"SHARE_ALL_SYSTEMS\")\n\n\t// allow all users to access systems if SHARE_ALL_SYSTEMS is set\n\tsystemsCollection, err := app.FindCollectionByNameOrId(\"systems\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar systemsReadRule string\n\tif shareAllSystems == \"true\" {\n\t\tsystemsReadRule = \"@request.auth.id != \\\"\\\"\"\n\t} else {\n\t\tsystemsReadRule = \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id\"\n\t}\n\tupdateDeleteRule := systemsReadRule + \" && @request.auth.role != \\\"readonly\\\"\"\n\tsystemsCollection.ListRule = &systemsReadRule\n\tsystemsCollection.ViewRule = &systemsReadRule\n\tsystemsCollection.UpdateRule = &updateDeleteRule\n\tsystemsCollection.DeleteRule = &updateDeleteRule\n\tif err := app.Save(systemsCollection); err != nil {\n\t\treturn err\n\t}\n\n\t// allow all users to access all containers if SHARE_ALL_SYSTEMS is set\n\tcontainersCollection, err := app.FindCollectionByNameOrId(\"containers\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tcontainersListRule := strings.Replace(systemsReadRule, \"users.id\", \"system.users.id\", 1)\n\tcontainersCollection.ListRule = &containersListRule\n\tif err := app.Save(containersCollection); err != nil {\n\t\treturn err\n\t}\n\n\t// allow all users to access system-related collections if SHARE_ALL_SYSTEMS is set\n\t// these collections all have a \"system\" relation field\n\tsystemRelatedCollections := []string{\"system_details\", \"smart_devices\", \"systemd_services\"}\n\tfor _, collectionName := range systemRelatedCollections {\n\t\tcollection, err := app.FindCollectionByNameOrId(collectionName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcollection.ListRule = &containersListRule\n\t\t// set viewRule for collections that need it (system_details, smart_devices)\n\t\tif collection.ViewRule != nil {\n\t\t\tcollection.ViewRule = &containersListRule\n\t\t}\n\t\t// set deleteRule for smart_devices (allows user to dismiss disk warnings)\n\t\tif collectionName == \"smart_devices\" {\n\t\t\tdeleteRule := containersListRule + \" && @request.auth.role != \\\"readonly\\\"\"\n\t\t\tcollection.DeleteRule = &deleteRule\n\t\t}\n\t\tif err := app.Save(collection); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// registerCronJobs sets up scheduled tasks\nfunc (h *Hub) registerCronJobs(_ *core.ServeEvent) error {\n\t// delete old system_stats and alerts_history records once every hour\n\th.Cron().MustAdd(\"delete old records\", \"8 * * * *\", h.rm.DeleteOldRecords)\n\t// create longer records every 10 minutes\n\th.Cron().MustAdd(\"create longer records\", \"*/10 * * * *\", h.rm.CreateLongerRecords)\n\treturn nil\n}\n\n// custom middlewares\nfunc (h *Hub) registerMiddlewares(se *core.ServeEvent) {\n\t// authorizes request with user matching the provided email\n\tauthorizeRequestWithEmail := func(e *core.RequestEvent, email string) (err error) {\n\t\tif e.Auth != nil || email == \"\" {\n\t\t\treturn e.Next()\n\t\t}\n\t\tisAuthRefresh := e.Request.URL.Path == \"/api/collections/users/auth-refresh\" && e.Request.Method == http.MethodPost\n\t\te.Auth, err = e.App.FindFirstRecordByData(\"users\", \"email\", email)\n\t\tif err != nil || !isAuthRefresh {\n\t\t\treturn e.Next()\n\t\t}\n\t\t// auth refresh endpoint, make sure token is set in header\n\t\ttoken, _ := e.Auth.NewAuthToken()\n\t\te.Request.Header.Set(\"Authorization\", token)\n\t\treturn e.Next()\n\t}\n\t// authenticate with trusted header\n\tif autoLogin, _ := GetEnv(\"AUTO_LOGIN\"); autoLogin != \"\" {\n\t\tse.Router.BindFunc(func(e *core.RequestEvent) error {\n\t\t\treturn authorizeRequestWithEmail(e, autoLogin)\n\t\t})\n\t}\n\t// authenticate with trusted header\n\tif trustedHeader, _ := GetEnv(\"TRUSTED_AUTH_HEADER\"); trustedHeader != \"\" {\n\t\tse.Router.BindFunc(func(e *core.RequestEvent) error {\n\t\t\treturn authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))\n\t\t})\n\t}\n}\n\n// custom api routes\nfunc (h *Hub) registerApiRoutes(se *core.ServeEvent) error {\n\t// auth protected routes\n\tapiAuth := se.Router.Group(\"/api/beszel\")\n\tapiAuth.Bind(apis.RequireAuth())\n\t// auth optional routes\n\tapiNoAuth := se.Router.Group(\"/api/beszel\")\n\n\t// create first user endpoint only needed if no users exist\n\tif totalUsers, _ := se.App.CountRecords(\"users\"); totalUsers == 0 {\n\t\tapiNoAuth.POST(\"/create-user\", h.um.CreateFirstUser)\n\t}\n\t// check if first time setup on login page\n\tapiNoAuth.GET(\"/first-run\", func(e *core.RequestEvent) error {\n\t\ttotal, err := e.App.CountRecords(\"users\")\n\t\treturn e.JSON(http.StatusOK, map[string]bool{\"firstRun\": err == nil && total == 0})\n\t})\n\t// get public key and version\n\tapiAuth.GET(\"/getkey\", func(e *core.RequestEvent) error {\n\t\treturn e.JSON(http.StatusOK, map[string]string{\"key\": h.pubKey, \"v\": beszel.Version})\n\t})\n\t// send test notification\n\tapiAuth.POST(\"/test-notification\", h.SendTestNotification)\n\t// heartbeat status and test\n\tapiAuth.GET(\"/heartbeat-status\", h.getHeartbeatStatus)\n\tapiAuth.POST(\"/test-heartbeat\", h.testHeartbeat)\n\t// get config.yml content\n\tapiAuth.GET(\"/config-yaml\", config.GetYamlConfig)\n\t// handle agent websocket connection\n\tapiNoAuth.GET(\"/agent-connect\", h.handleAgentConnect)\n\t// get or create universal tokens\n\tapiAuth.GET(\"/universal-token\", h.getUniversalToken)\n\t// update / delete user alerts\n\tapiAuth.POST(\"/user-alerts\", alerts.UpsertUserAlerts)\n\tapiAuth.DELETE(\"/user-alerts\", alerts.DeleteUserAlerts)\n\t// refresh SMART devices for a system\n\tapiAuth.POST(\"/smart/refresh\", h.refreshSmartData)\n\t// get systemd service details\n\tapiAuth.GET(\"/systemd/info\", h.getSystemdInfo)\n\t// /containers routes\n\tif enabled, _ := GetEnv(\"CONTAINER_DETAILS\"); enabled != \"false\" {\n\t\t// get container logs\n\t\tapiAuth.GET(\"/containers/logs\", h.getContainerLogs)\n\t\t// get container info\n\t\tapiAuth.GET(\"/containers/info\", h.getContainerInfo)\n\t}\n\treturn nil\n}\n\n// Handler for universal token API endpoint (create, read, delete)\nfunc (h *Hub) getUniversalToken(e *core.RequestEvent) error {\n\ttokenMap := universalTokenMap.GetMap()\n\tuserID := e.Auth.Id\n\tquery := e.Request.URL.Query()\n\ttoken := query.Get(\"token\")\n\tenable := query.Get(\"enable\")\n\tpermanent := query.Get(\"permanent\")\n\n\t// helper for deleting any existing permanent token record for this user\n\tdeletePermanent := func() error {\n\t\trec, err := h.FindFirstRecordByFilter(\"universal_tokens\", \"user = {:user}\", dbx.Params{\"user\": userID})\n\t\tif err != nil {\n\t\t\treturn nil // no record\n\t\t}\n\t\treturn h.Delete(rec)\n\t}\n\n\t// helper for upserting a permanent token record for this user\n\tupsertPermanent := func(token string) error {\n\t\trec, err := h.FindFirstRecordByFilter(\"universal_tokens\", \"user = {:user}\", dbx.Params{\"user\": userID})\n\t\tif err == nil {\n\t\t\trec.Set(\"token\", token)\n\t\t\treturn h.Save(rec)\n\t\t}\n\n\t\tcol, err := h.FindCachedCollectionByNameOrId(\"universal_tokens\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewRec := core.NewRecord(col)\n\t\tnewRec.Set(\"user\", userID)\n\t\tnewRec.Set(\"token\", token)\n\t\treturn h.Save(newRec)\n\t}\n\n\t// Disable universal tokens (both ephemeral and permanent)\n\tif enable == \"0\" {\n\t\ttokenMap.RemovebyValue(userID)\n\t\t_ = deletePermanent()\n\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": token, \"active\": false, \"permanent\": false})\n\t}\n\n\t// Enable universal token (ephemeral or permanent)\n\tif enable == \"1\" {\n\t\tif token == \"\" {\n\t\t\ttoken = uuid.New().String()\n\t\t}\n\n\t\tif permanent == \"1\" {\n\t\t\t// make token permanent (persist across restarts)\n\t\t\ttokenMap.RemovebyValue(userID)\n\t\t\tif err := upsertPermanent(token); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": token, \"active\": true, \"permanent\": true})\n\t\t}\n\n\t\t// default: ephemeral mode (1 hour)\n\t\t_ = deletePermanent()\n\t\ttokenMap.Set(token, userID, time.Hour)\n\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": token, \"active\": true, \"permanent\": false})\n\t}\n\n\t// Read current state\n\t// Prefer permanent token if it exists.\n\tif rec, err := h.FindFirstRecordByFilter(\"universal_tokens\", \"user = {:user}\", dbx.Params{\"user\": userID}); err == nil {\n\t\tdbToken := rec.GetString(\"token\")\n\t\t// If no token was provided, or the caller is asking about their permanent token, return it.\n\t\tif token == \"\" || token == dbToken {\n\t\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": dbToken, \"active\": true, \"permanent\": true})\n\t\t}\n\t\t// Token doesn't match their permanent token (avoid leaking other info)\n\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": token, \"active\": false, \"permanent\": false})\n\t}\n\n\t// No permanent token; fall back to ephemeral token map.\n\tif token == \"\" {\n\t\t// return existing token if it exists\n\t\tif token, _, ok := tokenMap.GetByValue(userID); ok {\n\t\t\treturn e.JSON(http.StatusOK, map[string]any{\"token\": token, \"active\": true, \"permanent\": false})\n\t\t}\n\t\t// if no token is provided, generate a new one\n\t\ttoken = uuid.New().String()\n\t}\n\n\t// Token is considered active only if it belongs to the current user.\n\tactiveUser, ok := tokenMap.GetOk(token)\n\tactive := ok && activeUser == userID\n\tresponse := map[string]any{\"token\": token, \"active\": active, \"permanent\": false}\n\treturn e.JSON(http.StatusOK, response)\n}\n\n// getHeartbeatStatus returns current heartbeat configuration and whether it's enabled\nfunc (h *Hub) getHeartbeatStatus(e *core.RequestEvent) error {\n\tif e.Auth.GetString(\"role\") != \"admin\" {\n\t\treturn e.ForbiddenError(\"Requires admin role\", nil)\n\t}\n\tif h.hb == nil {\n\t\treturn e.JSON(http.StatusOK, map[string]any{\n\t\t\t\"enabled\": false,\n\t\t\t\"msg\":     \"Set HEARTBEAT_URL to enable outbound heartbeat monitoring\",\n\t\t})\n\t}\n\tcfg := h.hb.GetConfig()\n\treturn e.JSON(http.StatusOK, map[string]any{\n\t\t\"enabled\":  true,\n\t\t\"url\":      cfg.URL,\n\t\t\"interval\": cfg.Interval,\n\t\t\"method\":   cfg.Method,\n\t})\n}\n\n// testHeartbeat triggers a single heartbeat ping and returns the result\nfunc (h *Hub) testHeartbeat(e *core.RequestEvent) error {\n\tif e.Auth.GetString(\"role\") != \"admin\" {\n\t\treturn e.ForbiddenError(\"Requires admin role\", nil)\n\t}\n\tif h.hb == nil {\n\t\treturn e.JSON(http.StatusOK, map[string]any{\n\t\t\t\"err\": \"Heartbeat not configured. Set HEARTBEAT_URL environment variable.\",\n\t\t})\n\t}\n\tif err := h.hb.Send(); err != nil {\n\t\treturn e.JSON(http.StatusOK, map[string]any{\"err\": err.Error()})\n\t}\n\treturn e.JSON(http.StatusOK, map[string]any{\"err\": false})\n}\n\n// containerRequestHandler handles both container logs and info requests\nfunc (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*systems.System, string) (string, error), responseKey string) error {\n\tsystemID := e.Request.URL.Query().Get(\"system\")\n\tcontainerID := e.Request.URL.Query().Get(\"container\")\n\n\tif systemID == \"\" || containerID == \"\" {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"error\": \"system and container parameters are required\"})\n\t}\n\tif !containerIDPattern.MatchString(containerID) {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"error\": \"invalid container parameter\"})\n\t}\n\n\tsystem, err := h.sm.GetSystem(systemID)\n\tif err != nil {\n\t\treturn e.JSON(http.StatusNotFound, map[string]string{\"error\": \"system not found\"})\n\t}\n\n\tdata, err := fetchFunc(system, containerID)\n\tif err != nil {\n\t\treturn e.JSON(http.StatusNotFound, map[string]string{\"error\": err.Error()})\n\t}\n\n\treturn e.JSON(http.StatusOK, map[string]string{responseKey: data})\n}\n\n// getContainerLogs handles GET /api/beszel/containers/logs requests\nfunc (h *Hub) getContainerLogs(e *core.RequestEvent) error {\n\treturn h.containerRequestHandler(e, func(system *systems.System, containerID string) (string, error) {\n\t\treturn system.FetchContainerLogsFromAgent(containerID)\n\t}, \"logs\")\n}\n\nfunc (h *Hub) getContainerInfo(e *core.RequestEvent) error {\n\treturn h.containerRequestHandler(e, func(system *systems.System, containerID string) (string, error) {\n\t\treturn system.FetchContainerInfoFromAgent(containerID)\n\t}, \"info\")\n}\n\n// getSystemdInfo handles GET /api/beszel/systemd/info requests\nfunc (h *Hub) getSystemdInfo(e *core.RequestEvent) error {\n\tquery := e.Request.URL.Query()\n\tsystemID := query.Get(\"system\")\n\tserviceName := query.Get(\"service\")\n\n\tif systemID == \"\" || serviceName == \"\" {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"error\": \"system and service parameters are required\"})\n\t}\n\tsystem, err := h.sm.GetSystem(systemID)\n\tif err != nil {\n\t\treturn e.JSON(http.StatusNotFound, map[string]string{\"error\": \"system not found\"})\n\t}\n\tdetails, err := system.FetchSystemdInfoFromAgent(serviceName)\n\tif err != nil {\n\t\treturn e.JSON(http.StatusNotFound, map[string]string{\"error\": err.Error()})\n\t}\n\te.Response.Header().Set(\"Cache-Control\", \"public, max-age=60\")\n\treturn e.JSON(http.StatusOK, map[string]any{\"details\": details})\n}\n\n// refreshSmartData handles POST /api/beszel/smart/refresh requests\n// Fetches fresh SMART data from the agent and updates the collection\nfunc (h *Hub) refreshSmartData(e *core.RequestEvent) error {\n\tsystemID := e.Request.URL.Query().Get(\"system\")\n\tif systemID == \"\" {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"error\": \"system parameter is required\"})\n\t}\n\n\tsystem, err := h.sm.GetSystem(systemID)\n\tif err != nil {\n\t\treturn e.JSON(http.StatusNotFound, map[string]string{\"error\": \"system not found\"})\n\t}\n\n\t// Fetch and save SMART devices\n\tif err := system.FetchAndSaveSmartDevices(); err != nil {\n\t\treturn e.JSON(http.StatusInternalServerError, map[string]string{\"error\": err.Error()})\n\t}\n\n\treturn e.JSON(http.StatusOK, map[string]string{\"status\": \"ok\"})\n}\n\n// generates key pair if it doesn't exist and returns signer\nfunc (h *Hub) GetSSHKey(dataDir string) (ssh.Signer, error) {\n\tif h.signer != nil {\n\t\treturn h.signer, nil\n\t}\n\n\tif dataDir == \"\" {\n\t\tdataDir = h.DataDir()\n\t}\n\n\tprivateKeyPath := path.Join(dataDir, \"id_ed25519\")\n\n\t// check if the key pair already exists\n\texistingKey, err := os.ReadFile(privateKeyPath)\n\tif err == nil {\n\t\tprivate, err := ssh.ParsePrivateKey(existingKey)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse private key: %s\", err)\n\t\t}\n\t\tpubKeyBytes := ssh.MarshalAuthorizedKey(private.PublicKey())\n\t\th.pubKey = strings.TrimSuffix(string(pubKeyBytes), \"\\n\")\n\t\treturn private, nil\n\t} else if !os.IsNotExist(err) {\n\t\t// File exists but couldn't be read for some other reason\n\t\treturn nil, fmt.Errorf(\"failed to read %s: %w\", privateKeyPath, err)\n\t}\n\n\t// Generate the Ed25519 key pair\n\t_, privKey, err := ed25519.GenerateKey(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprivKeyPem, err := ssh.MarshalPrivateKey(privKey, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := os.WriteFile(privateKeyPath, pem.EncodeToMemory(privKeyPem), 0600); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write private key to %q: err: %w\", privateKeyPath, err)\n\t}\n\n\t// These are fine to ignore the errors on, as we've literally just created a crypto.PublicKey | crypto.Signer\n\tsshPrivate, _ := ssh.NewSignerFromSigner(privKey)\n\tpubKeyBytes := ssh.MarshalAuthorizedKey(sshPrivate.PublicKey())\n\th.pubKey = strings.TrimSuffix(string(pubKeyBytes), \"\\n\")\n\n\th.Logger().Info(\"ed25519 key pair generated successfully.\")\n\th.Logger().Info(\"Saved to: \" + privateKeyPath)\n\n\treturn sshPrivate, err\n}\n\n// MakeLink formats a link with the app URL and path segments.\n// Only path segments should be provided.\nfunc (h *Hub) MakeLink(parts ...string) string {\n\tbase := strings.TrimSuffix(h.Settings().Meta.AppURL, \"/\")\n\tfor _, part := range parts {\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbase = fmt.Sprintf(\"%s/%s\", base, url.PathEscape(part))\n\t}\n\treturn base\n}\n"
  },
  {
    "path": "internal/hub/hub_test.go",
    "content": "//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/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/migrations\"\n\tbeszelTests \"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tpbTests \"github.com/pocketbase/pocketbase/tests\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// marshal to json and return an io.Reader (for use in ApiScenario.Body)\nfunc jsonReader(v any) io.Reader {\n\tdata, err := json.Marshal(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn bytes.NewReader(data)\n}\n\nfunc TestMakeLink(t *testing.T) {\n\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\n\ttests := []struct {\n\t\tname     string\n\t\tappURL   string\n\t\tparts    []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no parts, no trailing slash in AppURL\",\n\t\t\tappURL:   \"http://localhost:8090\",\n\t\t\tparts:    []string{},\n\t\t\texpected: \"http://localhost:8090\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no parts, with trailing slash in AppURL\",\n\t\t\tappURL:   \"http://localhost:8090/\",\n\t\t\tparts:    []string{},\n\t\t\texpected: \"http://localhost:8090\", // TrimSuffix should handle the trailing slash\n\t\t},\n\t\t{\n\t\t\tname:     \"one part\",\n\t\t\tappURL:   \"http://example.com\",\n\t\t\tparts:    []string{\"one\"},\n\t\t\texpected: \"http://example.com/one\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple parts\",\n\t\t\tappURL:   \"http://example.com\",\n\t\t\tparts:    []string{\"alpha\", \"beta\", \"gamma\"},\n\t\t\texpected: \"http://example.com/alpha/beta/gamma\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parts with spaces needing escaping\",\n\t\t\tappURL:   \"http://example.com\",\n\t\t\tparts:    []string{\"path with spaces\", \"another part\"},\n\t\t\texpected: \"http://example.com/path%20with%20spaces/another%20part\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parts with slashes needing escaping\",\n\t\t\tappURL:   \"http://example.com\",\n\t\t\tparts:    []string{\"a/b\", \"c\"},\n\t\t\texpected: \"http://example.com/a%2Fb/c\", // url.PathEscape escapes '/'\n\t\t},\n\t\t{\n\t\t\tname:     \"AppURL with subpath, no trailing slash\",\n\t\t\tappURL:   \"http://localhost/sub\",\n\t\t\tparts:    []string{\"resource\"},\n\t\t\texpected: \"http://localhost/sub/resource\",\n\t\t},\n\t\t{\n\t\t\tname:     \"AppURL with subpath, with trailing slash\",\n\t\t\tappURL:   \"http://localhost/sub/\",\n\t\t\tparts:    []string{\"item\"},\n\t\t\texpected: \"http://localhost/sub/item\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty parts in the middle\",\n\t\t\tappURL:   \"http://localhost\",\n\t\t\tparts:    []string{\"first\", \"\", \"third\"},\n\t\t\texpected: \"http://localhost/first/third\",\n\t\t},\n\t\t{\n\t\t\tname:     \"leading and trailing empty parts\",\n\t\t\tappURL:   \"http://localhost\",\n\t\t\tparts:    []string{\"\", \"path\", \"\"},\n\t\t\texpected: \"http://localhost/path\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parts with various special characters\",\n\t\t\tappURL:   \"https://test.dev/\",\n\t\t\tparts:    []string{\"p@th?\", \"key=value&\"},\n\t\t\texpected: \"https://test.dev/p@th%3F/key=value&\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Store original app URL and restore it after the test\n\t\t\toriginalAppURL := hub.Settings().Meta.AppURL\n\t\t\thub.Settings().Meta.AppURL = tt.appURL\n\t\t\tdefer func() { hub.Settings().Meta.AppURL = originalAppURL }()\n\n\t\t\tgot := hub.MakeLink(tt.parts...)\n\t\t\tassert.Equal(t, tt.expected, got, \"MakeLink generated URL does not match expected\")\n\t\t})\n\t}\n}\n\nfunc TestGetSSHKey(t *testing.T) {\n\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\n\t// Test Case 1: Key generation (no existing key)\n\tt.Run(\"KeyGeneration\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\n\t\t// Ensure pubKey is initially empty or different to ensure GetSSHKey sets it\n\t\thub.SetPubkey(\"\")\n\n\t\tsigner, err := hub.GetSSHKey(tempDir)\n\t\tassert.NoError(t, err, \"GetSSHKey should not error when generating a new key\")\n\t\tassert.NotNil(t, signer, \"GetSSHKey should return a non-nil signer\")\n\n\t\t// Check if private key file was created\n\t\tprivateKeyPath := filepath.Join(tempDir, \"id_ed25519\")\n\t\tinfo, err := os.Stat(privateKeyPath)\n\t\tassert.NoError(t, err, \"Private key file should be created\")\n\t\tassert.False(t, info.IsDir(), \"Private key path should be a file, not a directory\")\n\n\t\t// Check if h.pubKey was set\n\t\tassert.NotEmpty(t, hub.GetPubkey(), \"h.pubKey should be set after key generation\")\n\t\tassert.True(t, strings.HasPrefix(hub.GetPubkey(), \"ssh-ed25519 \"), \"h.pubKey should start with 'ssh-ed25519 '\")\n\n\t\t// Verify the generated private key is parsable\n\t\tkeyData, err := os.ReadFile(privateKeyPath)\n\t\trequire.NoError(t, err)\n\t\t_, err = ssh.ParsePrivateKey(keyData)\n\t\tassert.NoError(t, err, \"Generated private key should be parsable by ssh.ParsePrivateKey\")\n\t})\n\n\t// Test Case 2: Existing key\n\tt.Run(\"ExistingKey\", func(t *testing.T) {\n\t\ttempDir := t.TempDir()\n\n\t\t// Manually create a valid key pair for the test\n\t\trawPubKey, rawPrivKey, err := ed25519.GenerateKey(nil)\n\t\trequire.NoError(t, err, \"Failed to generate raw ed25519 key pair for pre-existing key test\")\n\n\t\t// Marshal the private key into OpenSSH PEM format\n\t\tpemBlock, err := ssh.MarshalPrivateKey(rawPrivKey, \"\")\n\t\trequire.NoError(t, err, \"Failed to marshal private key to PEM block for pre-existing key test\")\n\n\t\tprivateKeyBytes := pem.EncodeToMemory(pemBlock)\n\t\trequire.NotNil(t, privateKeyBytes, \"PEM encoded private key bytes should not be nil\")\n\n\t\tprivateKeyPath := filepath.Join(tempDir, \"id_ed25519\")\n\t\terr = os.WriteFile(privateKeyPath, privateKeyBytes, 0600)\n\t\trequire.NoError(t, err, \"Failed to write pre-existing private key\")\n\n\t\t// Determine the expected public key string\n\t\tsshPubKey, err := ssh.NewPublicKey(rawPubKey)\n\t\trequire.NoError(t, err)\n\t\texpectedPubKeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPubKey)))\n\n\t\t// Reset h.pubKey to ensure it's set by GetSSHKey from the file\n\t\thub.SetPubkey(\"\")\n\n\t\tsigner, err := hub.GetSSHKey(tempDir)\n\t\tassert.NoError(t, err, \"GetSSHKey should not error when reading an existing key\")\n\t\tassert.NotNil(t, signer, \"GetSSHKey should return a non-nil signer for an existing key\")\n\n\t\t// Check if h.pubKey was set correctly to the public key from the file\n\t\tassert.Equal(t, expectedPubKeyStr, hub.GetPubkey(), \"h.pubKey should match the existing public key\")\n\n\t\t// Verify the signer's public key matches the original public key\n\t\tsignerPubKey := signer.PublicKey()\n\t\tmarshaledSignerPubKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signerPubKey)))\n\t\tassert.Equal(t, expectedPubKeyStr, marshaledSignerPubKey, \"Signer's public key should match the existing public key\")\n\t})\n\n\t// Test Case 3: Error cases\n\tt.Run(\"ErrorCases\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\tname       string\n\t\t\tsetupFunc  func(dir string) error\n\t\t\terrorCheck func(t *testing.T, err error)\n\t\t}{\n\t\t\t{\n\t\t\t\tname: \"CorruptedKey\",\n\t\t\t\tsetupFunc: func(dir string) error {\n\t\t\t\t\treturn os.WriteFile(filepath.Join(dir, \"id_ed25519\"), []byte(\"this is not a valid SSH key\"), 0600)\n\t\t\t\t},\n\t\t\t\terrorCheck: func(t *testing.T, err error) {\n\t\t\t\t\tassert.Error(t, err)\n\t\t\t\t\tassert.Contains(t, err.Error(), \"ssh: no key found\")\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"PermissionDenied\",\n\t\t\t\tsetupFunc: func(dir string) error {\n\t\t\t\t\t// Create the key file\n\t\t\t\t\tkeyPath := filepath.Join(dir, \"id_ed25519\")\n\t\t\t\t\tif err := os.WriteFile(keyPath, []byte(\"dummy content\"), 0600); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\t// Make it read-only (can't be opened for writing in case a new key needs to be written)\n\t\t\t\t\treturn os.Chmod(keyPath, 0400)\n\t\t\t\t},\n\t\t\t\terrorCheck: func(t *testing.T, err error) {\n\t\t\t\t\t// On read-only key, the parser will attempt to parse it and fail with \"ssh: no key found\"\n\t\t\t\t\tassert.Error(t, err)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"EmptyFile\",\n\t\t\t\tsetupFunc: func(dir string) error {\n\t\t\t\t\t// Create an empty file\n\t\t\t\t\treturn os.WriteFile(filepath.Join(dir, \"id_ed25519\"), []byte{}, 0600)\n\t\t\t\t},\n\t\t\t\terrorCheck: func(t *testing.T, err error) {\n\t\t\t\t\tassert.Error(t, err)\n\t\t\t\t\t// The error from attempting to parse an empty file\n\t\t\t\t\tassert.Contains(t, err.Error(), \"ssh: no key found\")\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\ttempDir := t.TempDir()\n\n\t\t\t\t// Setup the test case\n\t\t\t\terr := tc.setupFunc(tempDir)\n\t\t\t\trequire.NoError(t, err, \"Setup failed\")\n\n\t\t\t\t// Reset h.pubKey before each test case\n\t\t\t\thub.SetPubkey(\"\")\n\n\t\t\t\t// Attempt to get SSH key\n\t\t\t\t_, err = hub.GetSSHKey(tempDir)\n\n\t\t\t\t// Verify the error\n\t\t\t\ttc.errorCheck(t, err)\n\n\t\t\t\t// Check that pubKey was not set in error cases\n\t\t\t\tassert.Empty(t, hub.GetPubkey(), \"h.pubKey should not be set if there was an error\")\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestApiRoutesAuthentication(t *testing.T) {\n\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\tdefer hub.Cleanup()\n\n\thub.StartHub()\n\n\t// Create test user and get auth token\n\tuser, err := beszelTests.CreateUser(hub, \"testuser@example.com\", \"password123\")\n\trequire.NoError(t, err, \"Failed to create test user\")\n\n\tadminUser, err := beszelTests.CreateRecord(hub, \"users\", map[string]any{\n\t\t\"email\":    \"admin@example.com\",\n\t\t\"password\": \"password123\",\n\t\t\"role\":     \"admin\",\n\t})\n\trequire.NoError(t, err, \"Failed to create admin user\")\n\tadminUserToken, err := adminUser.NewAuthToken()\n\n\t// superUser, err := beszelTests.CreateRecord(hub, core.CollectionNameSuperusers, map[string]any{\n\t// \t\"email\":    \"superuser@example.com\",\n\t// \t\"password\": \"password123\",\n\t// })\n\t// require.NoError(t, err, \"Failed to create superuser\")\n\n\tuserToken, err := user.NewAuthToken()\n\trequire.NoError(t, err, \"Failed to create auth token\")\n\n\t// Create test system for user-alerts endpoints\n\tsystem, err := beszelTests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":  \"test-system\",\n\t\t\"users\": []string{user.Id},\n\t\t\"host\":  \"127.0.0.1\",\n\t})\n\trequire.NoError(t, err, \"Failed to create test system\")\n\n\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\treturn hub.TestApp\n\t}\n\n\tscenarios := []beszelTests.ApiScenario{\n\t\t// Auth Protected Routes - Should require authentication\n\t\t{\n\t\t\tName:            \"POST /test-notification - no auth should fail\",\n\t\t\tMethod:          http.MethodPost,\n\t\t\tURL:             \"/api/beszel/test-notification\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"url\": \"generic://127.0.0.1\",\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tName:           \"POST /test-notification - with auth should succeed\",\n\t\t\tMethod:         http.MethodPost,\n\t\t\tURL:            \"/api/beszel/test-notification\",\n\t\t\tTestAppFactory: testAppFactory,\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"url\": \"generic://127.0.0.1\",\n\t\t\t}),\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"sending message\"},\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /config-yaml - no auth should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/config-yaml\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /config-yaml - with user auth should fail\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/config-yaml\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  403,\n\t\t\tExpectedContent: []string{\"Requires admin\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /config-yaml - with admin auth should succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/config-yaml\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": adminUserToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"test-system\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /heartbeat-status - no auth should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/heartbeat-status\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /heartbeat-status - with user auth should fail\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/heartbeat-status\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  403,\n\t\t\tExpectedContent: []string{\"Requires admin role\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /heartbeat-status - with admin auth should succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/heartbeat-status\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": adminUserToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{`\"enabled\":false`},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST /test-heartbeat - with user auth should fail\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/test-heartbeat\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  403,\n\t\t\tExpectedContent: []string{\"Requires admin role\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST /test-heartbeat - with admin auth should report disabled state\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/test-heartbeat\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": adminUserToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"Heartbeat not configured\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /universal-token - no auth should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/universal-token\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /universal-token - with auth should succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/universal-token\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"active\", \"token\", \"permanent\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /universal-token - enable permanent should succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/universal-token?enable=1&permanent=1&token=permanent-token-123\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"permanent\\\":true\", \"permanent-token-123\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"POST /user-alerts - no auth should fail\",\n\t\t\tMethod:          http.MethodPost,\n\t\t\tURL:             \"/api/beszel/user-alerts\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"value\":   80,\n\t\t\t\t\"min\":     10,\n\t\t\t\t\"systems\": []string{system.Id},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tName:   \"POST /user-alerts - with auth should succeed\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"value\":   80,\n\t\t\t\t\"min\":     10,\n\t\t\t\t\"systems\": []string{system.Id},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tName:            \"DELETE /user-alerts - no auth should fail\",\n\t\t\tMethod:          http.MethodDelete,\n\t\t\tURL:             \"/api/beszel/user-alerts\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"systems\": []string{system.Id},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tName:   \"DELETE /user-alerts - with auth should succeed\",\n\t\t\tMethod: http.MethodDelete,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"success\\\":true\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"systems\": []string{system.Id},\n\t\t\t}),\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\t// Create an alert to delete\n\t\t\t\tbeszelTests.CreateRecord(app, \"alerts\", map[string]any{\n\t\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\t\"system\": system.Id,\n\t\t\t\t\t\"user\":   user.Id,\n\t\t\t\t\t\"value\":  80,\n\t\t\t\t\t\"min\":    10,\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /containers/logs - no auth should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/containers/logs?system=test-system&container=test-container\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/logs - with auth but missing system param should fail\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/logs?container=test-container\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"system and container parameters are required\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/logs - with auth but missing container param should fail\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/logs?system=test-system\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"system and container parameters are required\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/logs - with auth but invalid system should fail\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/logs?system=invalid-system&container=0123456789ab\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  404,\n\t\t\tExpectedContent: []string{\"system not found\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/logs - traversal container should fail validation\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/logs?system=\" + system.Id + \"&container=..%2F..%2Fversion\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"invalid container parameter\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/info - traversal container should fail validation\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/info?system=\" + system.Id + \"&container=../../version?x=\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"invalid container parameter\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /containers/info - non-hex container should fail validation\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/containers/info?system=\" + system.Id + \"&container=container_name\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{\"invalid container parameter\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\n\t\t// Auth Optional Routes - Should work without authentication\n\t\t{\n\t\t\tName:            \"GET /getkey - no auth should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/getkey\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /getkey - with auth should also succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/getkey\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"key\\\":\", \"\\\"v\\\":\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /first-run - no auth should succeed\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/first-run\",\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"firstRun\\\":false\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /first-run - with auth should also succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/first-run\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": userToken,\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"firstRun\\\":false\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /agent-connect - no auth should succeed (websocket upgrade fails but route is accessible)\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/agent-connect\",\n\t\t\tExpectedStatus:  400,\n\t\t\tExpectedContent: []string{},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST /test-notification - invalid auth token should fail\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/test-notification\",\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"url\": \"generic://127.0.0.1\",\n\t\t\t}),\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": \"invalid-token\",\n\t\t\t},\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"POST /user-alerts - invalid auth token should fail\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/user-alerts\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"Authorization\": \"invalid-token\",\n\t\t\t},\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\"value\":   80,\n\t\t\t\t\"min\":     10,\n\t\t\t\t\"systems\": []string{system.Id},\n\t\t\t}),\n\t\t},\n\t}\n\n\tfor _, scenario := range scenarios {\n\t\tscenario.Test(t)\n\t}\n}\n\nfunc TestFirstUserCreation(t *testing.T) {\n\tt.Run(\"CreateUserEndpoint available when no users exist\", func(t *testing.T) {\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\tdefer hub.Cleanup()\n\n\t\thub.StartHub()\n\n\t\ttestAppFactoryExisting := func(t testing.TB) *pbTests.TestApp {\n\t\t\treturn hub.TestApp\n\t\t}\n\n\t\tscenarios := []beszelTests.ApiScenario{\n\t\t\t{\n\t\t\t\tName:   \"POST /create-user - should be available when no users exist\",\n\t\t\t\tMethod: http.MethodPost,\n\t\t\t\tURL:    \"/api/beszel/create-user\",\n\t\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\t\"email\":    \"firstuser@example.com\",\n\t\t\t\t\t\"password\": \"password123\",\n\t\t\t\t}),\n\t\t\t\tExpectedStatus:  200,\n\t\t\t\tExpectedContent: []string{\"User created\"},\n\t\t\t\tTestAppFactory:  testAppFactoryExisting,\n\t\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\t\tuserCount, err := hub.CountRecords(\"users\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Zero(t, userCount, \"Should start with no users\")\n\t\t\t\t\tsuperusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.EqualValues(t, 1, len(superusers), \"Should start with one temporary superuser\")\n\t\t\t\t\trequire.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString(\"email\"), \"Should have created one temporary superuser\")\n\t\t\t\t},\n\t\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\t\tuserCount, err := hub.CountRecords(\"users\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.EqualValues(t, 1, userCount, \"Should have created one user\")\n\t\t\t\t\tsuperusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.EqualValues(t, 1, len(superusers), \"Should have created one superuser\")\n\t\t\t\t\trequire.EqualValues(t, \"firstuser@example.com\", superusers[0].GetString(\"email\"), \"Should have created one superuser\")\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:   \"POST /create-user - should not be available when users exist\",\n\t\t\t\tMethod: http.MethodPost,\n\t\t\t\tURL:    \"/api/beszel/create-user\",\n\t\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\t\"email\":    \"firstuser@example.com\",\n\t\t\t\t\t\"password\": \"password123\",\n\t\t\t\t}),\n\t\t\t\tExpectedStatus:  404,\n\t\t\t\tExpectedContent: []string{\"wasn't found\"},\n\t\t\t\tTestAppFactory:  testAppFactoryExisting,\n\t\t\t},\n\t\t}\n\n\t\tfor _, scenario := range scenarios {\n\t\t\tscenario.Test(t)\n\t\t}\n\t})\n\n\tt.Run(\"CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set\", func(t *testing.T) {\n\t\tos.Setenv(\"BESZEL_HUB_USER_EMAIL\", \"me@example.com\")\n\t\tos.Setenv(\"BESZEL_HUB_USER_PASSWORD\", \"password123\")\n\t\tdefer os.Unsetenv(\"BESZEL_HUB_USER_EMAIL\")\n\t\tdefer os.Unsetenv(\"BESZEL_HUB_USER_PASSWORD\")\n\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\tdefer hub.Cleanup()\n\n\t\thub.StartHub()\n\n\t\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\t\treturn hub.TestApp\n\t\t}\n\n\t\tscenario := beszelTests.ApiScenario{\n\t\t\tName:            \"POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set\",\n\t\t\tMethod:          http.MethodPost,\n\t\t\tURL:             \"/api/beszel/create-user\",\n\t\t\tExpectedStatus:  404,\n\t\t\tExpectedContent: []string{\"wasn't found\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tusers, err := hub.FindAllRecords(\"users\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.EqualValues(t, 1, len(users), \"Should start with one user\")\n\t\t\t\trequire.EqualValues(t, \"me@example.com\", users[0].GetString(\"email\"), \"Should have created one user\")\n\t\t\t\tsuperusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.EqualValues(t, 1, len(superusers), \"Should start with one superuser\")\n\t\t\t\trequire.EqualValues(t, \"me@example.com\", superusers[0].GetString(\"email\"), \"Should have created one superuser\")\n\t\t\t},\n\t\t\tAfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {\n\t\t\t\tusers, err := hub.FindAllRecords(\"users\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.EqualValues(t, 1, len(users), \"Should still have one user\")\n\t\t\t\trequire.EqualValues(t, \"me@example.com\", users[0].GetString(\"email\"), \"Should have created one user\")\n\t\t\t\tsuperusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.EqualValues(t, 1, len(superusers), \"Should still have one superuser\")\n\t\t\t\trequire.EqualValues(t, \"me@example.com\", superusers[0].GetString(\"email\"), \"Should have created one superuser\")\n\t\t\t},\n\t\t}\n\n\t\tscenario.Test(t)\n\t})\n}\n\nfunc TestCreateUserEndpointAvailability(t *testing.T) {\n\tt.Run(\"CreateUserEndpoint available when no users exist\", func(t *testing.T) {\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\tdefer hub.Cleanup()\n\n\t\t// Ensure no users exist\n\t\tuserCount, err := hub.CountRecords(\"users\")\n\t\trequire.NoError(t, err)\n\t\trequire.Zero(t, userCount, \"Should start with no users\")\n\n\t\thub.StartHub()\n\n\t\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\t\treturn hub.TestApp\n\t\t}\n\n\t\tscenario := beszelTests.ApiScenario{\n\t\t\tName:   \"POST /create-user - should be available when no users exist\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/create-user\",\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"email\":    \"firstuser@example.com\",\n\t\t\t\t\"password\": \"password123\",\n\t\t\t}),\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"User created\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t}\n\n\t\tscenario.Test(t)\n\n\t\t// Verify user was created\n\t\tuserCount, err = hub.CountRecords(\"users\")\n\t\trequire.NoError(t, err)\n\t\trequire.EqualValues(t, 1, userCount, \"Should have created one user\")\n\t})\n\n\tt.Run(\"CreateUserEndpoint not available when users exist\", func(t *testing.T) {\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\tdefer hub.Cleanup()\n\n\t\t// Create a user first\n\t\t_, err := beszelTests.CreateUser(hub, \"existing@example.com\", \"password\")\n\t\trequire.NoError(t, err)\n\n\t\thub.StartHub()\n\n\t\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\t\treturn hub.TestApp\n\t\t}\n\n\t\tscenario := beszelTests.ApiScenario{\n\t\t\tName:   \"POST /create-user - should not be available when users exist\",\n\t\t\tMethod: http.MethodPost,\n\t\t\tURL:    \"/api/beszel/create-user\",\n\t\t\tBody: jsonReader(map[string]any{\n\t\t\t\t\"email\":    \"another@example.com\",\n\t\t\t\t\"password\": \"password123\",\n\t\t\t}),\n\t\t\tExpectedStatus:  404,\n\t\t\tExpectedContent: []string{\"wasn't found\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t}\n\n\t\tscenario.Test(t)\n\t})\n}\n\nfunc TestAutoLoginMiddleware(t *testing.T) {\n\tvar hubs []*beszelTests.TestHub\n\n\tdefer func() {\n\t\tdefer os.Unsetenv(\"AUTO_LOGIN\")\n\t\tfor _, hub := range hubs {\n\t\t\thub.Cleanup()\n\t\t}\n\t}()\n\n\tos.Setenv(\"AUTO_LOGIN\", \"user@test.com\")\n\n\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\thubs = append(hubs, hub)\n\t\thub.StartHub()\n\t\treturn hub.TestApp\n\t}\n\n\tscenarios := []beszelTests.ApiScenario{\n\t\t{\n\t\t\tName:            \"GET /getkey - without auto login should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/getkey\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /getkey - with auto login should fail if no matching user\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/getkey\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:            \"GET /getkey - with auto login should succeed\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/getkey\",\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"key\\\":\", \"\\\"v\\\":\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.CreateUser(app, \"user@test.com\", \"password123\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, scenario := range scenarios {\n\t\tscenario.Test(t)\n\t}\n}\n\nfunc TestTrustedHeaderMiddleware(t *testing.T) {\n\tvar hubs []*beszelTests.TestHub\n\n\tdefer func() {\n\t\tdefer os.Unsetenv(\"TRUSTED_AUTH_HEADER\")\n\t\tfor _, hub := range hubs {\n\t\t\thub.Cleanup()\n\t\t}\n\t}()\n\n\tos.Setenv(\"TRUSTED_AUTH_HEADER\", \"X-Beszel-Trusted\")\n\n\ttestAppFactory := func(t testing.TB) *pbTests.TestApp {\n\t\thub, _ := beszelTests.NewTestHub(t.TempDir())\n\t\thubs = append(hubs, hub)\n\t\thub.StartHub()\n\t\treturn hub.TestApp\n\t}\n\n\tscenarios := []beszelTests.ApiScenario{\n\t\t{\n\t\t\tName:            \"GET /getkey - without trusted header should fail\",\n\t\t\tMethod:          http.MethodGet,\n\t\t\tURL:             \"/api/beszel/getkey\",\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /getkey - with trusted header should fail if no matching user\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/getkey\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"X-Beszel-Trusted\": \"user@test.com\",\n\t\t\t},\n\t\t\tExpectedStatus:  401,\n\t\t\tExpectedContent: []string{\"requires valid\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t},\n\t\t{\n\t\t\tName:   \"GET /getkey - with trusted header should succeed\",\n\t\t\tMethod: http.MethodGet,\n\t\t\tURL:    \"/api/beszel/getkey\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"X-Beszel-Trusted\": \"user@test.com\",\n\t\t\t},\n\t\t\tExpectedStatus:  200,\n\t\t\tExpectedContent: []string{\"\\\"key\\\":\", \"\\\"v\\\":\"},\n\t\t\tTestAppFactory:  testAppFactory,\n\t\t\tBeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {\n\t\t\t\tbeszelTests.CreateUser(app, \"user@test.com\", \"password123\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, scenario := range scenarios {\n\t\tscenario.Test(t)\n\t}\n}\n"
  },
  {
    "path": "internal/hub/hub_test_helpers.go",
    "content": "//go:build testing\n\npackage hub\n\nimport \"github.com/henrygd/beszel/internal/hub/systems\"\n\n// TESTING ONLY: GetSystemManager returns the system manager\nfunc (h *Hub) GetSystemManager() *systems.SystemManager {\n\treturn h.sm\n}\n\n// TESTING ONLY: GetPubkey returns the public key\nfunc (h *Hub) GetPubkey() string {\n\treturn h.pubKey\n}\n\n// TESTING ONLY: SetPubkey sets the public key\nfunc (h *Hub) SetPubkey(pubkey string) {\n\th.pubKey = pubkey\n}\n"
  },
  {
    "path": "internal/hub/server_development.go",
    "content": "//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\"strings\"\n\n\t\"github.com/henrygd/beszel\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/osutils\"\n)\n\n// Wraps http.RoundTripper to modify dev proxy HTML responses\ntype responseModifier struct {\n\ttransport http.RoundTripper\n\thub       *Hub\n}\n\nfunc (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) {\n\tresp, err := rm.transport.RoundTrip(req)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\t// Only modify HTML responses\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tif !strings.Contains(contentType, \"text/html\") {\n\t\treturn resp, nil\n\t}\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\tresp.Body.Close()\n\t// Create a new response with the modified body\n\tmodifiedBody := rm.modifyHTML(string(body))\n\tresp.Body = io.NopCloser(strings.NewReader(modifiedBody))\n\tresp.ContentLength = int64(len(modifiedBody))\n\tresp.Header.Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(modifiedBody)))\n\n\treturn resp, nil\n}\n\nfunc (rm *responseModifier) modifyHTML(html string) string {\n\tparsedURL, err := url.Parse(rm.hub.appURL)\n\tif err != nil {\n\t\treturn html\n\t}\n\t// fix base paths in html if using subpath\n\tbasePath := strings.TrimSuffix(parsedURL.Path, \"/\") + \"/\"\n\thtml = strings.ReplaceAll(html, \"./\", basePath)\n\thtml = strings.Replace(html, \"{{V}}\", beszel.Version, 1)\n\thtml = strings.Replace(html, \"{{HUB_URL}}\", rm.hub.appURL, 1)\n\treturn html\n}\n\n// startServer sets up the development server for Beszel\nfunc (h *Hub) startServer(se *core.ServeEvent) error {\n\tslog.Info(\"starting server\", \"appURL\", h.appURL)\n\tproxy := httputil.NewSingleHostReverseProxy(&url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   \"localhost:5173\",\n\t})\n\n\tproxy.Transport = &responseModifier{\n\t\ttransport: http.DefaultTransport,\n\t\thub:       h,\n\t}\n\n\tse.Router.GET(\"/{path...}\", func(e *core.RequestEvent) error {\n\t\tproxy.ServeHTTP(e.Response, e.Request)\n\t\treturn nil\n\t})\n\t_ = osutils.LaunchURL(h.appURL)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/hub/server_production.go",
    "content": "//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\t\"github.com/henrygd/beszel/internal/site\"\n\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// startServer sets up the production server for Beszel\nfunc (h *Hub) startServer(se *core.ServeEvent) error {\n\t// parse app url\n\tparsedURL, err := url.Parse(h.appURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// fix base paths in html if using subpath\n\tbasePath := strings.TrimSuffix(parsedURL.Path, \"/\") + \"/\"\n\tindexFile, _ := fs.ReadFile(site.DistDirFS, \"index.html\")\n\thtml := strings.ReplaceAll(string(indexFile), \"./\", basePath)\n\thtml = strings.Replace(html, \"{{V}}\", beszel.Version, 1)\n\thtml = strings.Replace(html, \"{{HUB_URL}}\", h.appURL, 1)\n\t// set up static asset serving\n\tstaticPaths := [2]string{\"/static/\", \"/assets/\"}\n\tserveStatic := apis.Static(site.DistDirFS, false)\n\t// get CSP configuration\n\tcsp, cspExists := GetEnv(\"CSP\")\n\t// add route\n\tse.Router.GET(\"/{path...}\", func(e *core.RequestEvent) error {\n\t\t// serve static assets if path is in staticPaths\n\t\tfor i := range staticPaths {\n\t\t\tif strings.Contains(e.Request.URL.Path, staticPaths[i]) {\n\t\t\t\te.Response.Header().Set(\"Cache-Control\", \"public, max-age=2592000\")\n\t\t\t\treturn serveStatic(e)\n\t\t\t}\n\t\t}\n\t\tif cspExists {\n\t\t\te.Response.Header().Del(\"X-Frame-Options\")\n\t\t\te.Response.Header().Set(\"Content-Security-Policy\", csp)\n\t\t}\n\t\treturn e.HTML(http.StatusOK, html)\n\t})\n\treturn nil\n}\n"
  },
  {
    "path": "internal/hub/systems/system.go",
    "content": "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/atomic\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/hub/transport\"\n\t\"github.com/henrygd/beszel/internal/hub/ws\"\n\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n\n\t\"github.com/henrygd/beszel\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/lxzan/gws\"\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype System struct {\n\tId             string                  `db:\"id\"`\n\tHost           string                  `db:\"host\"`\n\tPort           string                  `db:\"port\"`\n\tStatus         string                  `db:\"status\"`\n\tmanager        *SystemManager          // Manager that this system belongs to\n\tclient         *ssh.Client             // SSH client for fetching data\n\tsshTransport   *transport.SSHTransport // SSH transport for requests\n\tdata           *system.CombinedData    // system data from agent\n\tctx            context.Context         // Context for stopping the updater\n\tcancel         context.CancelFunc      // Stops and removes system from updater\n\tWsConn         *ws.WsConn              // Handler for agent WebSocket connection\n\tagentVersion   semver.Version          // Agent version\n\tupdateTicker   *time.Ticker            // Ticker for updating the system\n\tdetailsFetched atomic.Bool             // True if static system details have been fetched and saved\n\tsmartFetching  atomic.Bool             // True if SMART devices are currently being fetched\n\tsmartInterval  time.Duration           // Interval for periodic SMART data updates\n}\n\nfunc (sm *SystemManager) NewSystem(systemId string) *System {\n\tsystem := &System{\n\t\tId:   systemId,\n\t\tdata: &system.CombinedData{},\n\t}\n\tsystem.ctx, system.cancel = system.getContext()\n\treturn system\n}\n\n// StartUpdater starts the system updater.\n// It first fetches the data from the agent then updates the records.\n// If the data is not found or the system is down, it sets the system down.\nfunc (sys *System) StartUpdater() {\n\t// Channel that can be used to set the system down. Currently only used to\n\t// allow a short delay for reconnection after websocket connection is closed.\n\tvar downChan chan struct{}\n\n\t// Add random jitter to first WebSocket connection to prevent\n\t// clustering if all agents are started at the same time.\n\t// SSH connections during hub startup are already staggered.\n\tvar jitter <-chan time.Time\n\tif sys.WsConn != nil {\n\t\tjitter = getJitter()\n\t\t// use the websocket connection's down channel to set the system down\n\t\tdownChan = sys.WsConn.DownChan\n\t} else {\n\t\t// if the system does not have a websocket connection, wait before updating\n\t\t// to allow the agent to connect via websocket (makes sure fingerprint is set).\n\t\ttime.Sleep(11 * time.Second)\n\t}\n\n\t// update immediately if system is not paused (only for ws connections)\n\t// we'll wait a minute before connecting via SSH to prioritize ws connections\n\tif sys.Status != paused && sys.ctx.Err() == nil {\n\t\tif err := sys.update(); err != nil {\n\t\t\t_ = sys.setDown(err)\n\t\t}\n\t}\n\n\tsys.updateTicker = time.NewTicker(time.Duration(interval) * time.Millisecond)\n\t// Go 1.23+ will automatically stop the ticker when the system is garbage collected, however we seem to need this or testing/synctest will block even if calling runtime.GC()\n\tdefer sys.updateTicker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-sys.ctx.Done():\n\t\t\treturn\n\t\tcase <-sys.updateTicker.C:\n\t\t\tif err := sys.update(); err != nil {\n\t\t\t\t_ = sys.setDown(err)\n\t\t\t}\n\t\tcase <-downChan:\n\t\t\tsys.WsConn = nil\n\t\t\tdownChan = nil\n\t\t\t_ = sys.setDown(nil)\n\t\tcase <-jitter:\n\t\t\tsys.updateTicker.Reset(time.Duration(interval) * time.Millisecond)\n\t\t\tif err := sys.update(); err != nil {\n\t\t\t\t_ = sys.setDown(err)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// update updates the system data and records.\nfunc (sys *System) update() error {\n\tif sys.Status == paused {\n\t\tsys.handlePaused()\n\t\treturn nil\n\t}\n\toptions := common.DataRequestOptions{\n\t\tCacheTimeMs: uint16(interval),\n\t}\n\t// fetch system details if not already fetched\n\tif !sys.detailsFetched.Load() {\n\t\toptions.IncludeDetails = true\n\t}\n\n\tdata, err := sys.fetchDataFromAgent(options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// ensure deprecated fields from older agents are migrated to current fields\n\tmigrateDeprecatedFields(data, !sys.detailsFetched.Load())\n\n\t// create system records\n\t_, err = sys.createRecords(data)\n\n\t// if details were included and fetched successfully, mark details as fetched and update smart interval if set by agent\n\tif err == nil && data.Details != nil {\n\t\tsys.detailsFetched.Store(true)\n\t\t// update smart interval if it's set on the agent side\n\t\tif data.Details.SmartInterval > 0 {\n\t\t\tsys.smartInterval = data.Details.SmartInterval\n\t\t\t// make sure we reset expiration of lastFetch to remain as long as the new smart interval\n\t\t\t// to prevent premature expiration leading to new fetch if interval is different.\n\t\t\tsys.manager.smartFetchMap.UpdateExpiration(sys.Id, sys.smartInterval+time.Minute)\n\t\t}\n\t}\n\n\t// Fetch and save SMART devices when system first comes online or at intervals\n\tif backgroundSmartFetchEnabled() && sys.detailsFetched.Load() {\n\t\tif sys.smartInterval <= 0 {\n\t\t\tsys.smartInterval = time.Hour\n\t\t}\n\t\tlastFetch, _ := sys.manager.smartFetchMap.GetOk(sys.Id)\n\t\tif time.Since(time.UnixMilli(lastFetch-1e4)) >= sys.smartInterval && sys.smartFetching.CompareAndSwap(false, true) {\n\t\t\tgo func() {\n\t\t\t\tdefer sys.smartFetching.Store(false)\n\t\t\t\tsys.manager.smartFetchMap.Set(sys.Id, time.Now().UnixMilli(), sys.smartInterval+time.Minute)\n\t\t\t\t_ = sys.FetchAndSaveSmartDevices()\n\t\t\t}()\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (sys *System) handlePaused() {\n\tif sys.WsConn == nil {\n\t\t// if the system is paused and there's no websocket connection, remove the system\n\t\t_ = sys.manager.RemoveSystem(sys.Id)\n\t} else {\n\t\t// Send a ping to the agent to keep the connection alive if the system is paused\n\t\tif err := sys.WsConn.Ping(); err != nil {\n\t\t\tsys.manager.hub.Logger().Warn(\"Failed to ping agent\", \"system\", sys.Id, \"err\", err)\n\t\t\t_ = sys.manager.RemoveSystem(sys.Id)\n\t\t}\n\t}\n}\n\n// createRecords updates the system record and adds system_stats and container_stats records\nfunc (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {\n\tsystemRecord, err := sys.getRecord()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thub := sys.manager.hub\n\terr = hub.RunInTransaction(func(txApp core.App) error {\n\t\t// add system_stats record\n\t\tsystemStatsCollection, err := txApp.FindCachedCollectionByNameOrId(\"system_stats\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsystemStatsRecord := core.NewRecord(systemStatsCollection)\n\t\tsystemStatsRecord.Set(\"system\", systemRecord.Id)\n\t\tsystemStatsRecord.Set(\"stats\", data.Stats)\n\t\tsystemStatsRecord.Set(\"type\", \"1m\")\n\t\tif err := txApp.SaveNoValidate(systemStatsRecord); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// add containers and container_stats records\n\t\tif len(data.Containers) > 0 {\n\t\t\tif data.Containers[0].Id != \"\" {\n\t\t\t\tif err := createContainerRecords(txApp, data.Containers, sys.Id); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontainerStatsCollection, err := txApp.FindCachedCollectionByNameOrId(\"container_stats\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontainerStatsRecord := core.NewRecord(containerStatsCollection)\n\t\t\tcontainerStatsRecord.Set(\"system\", systemRecord.Id)\n\t\t\tcontainerStatsRecord.Set(\"stats\", data.Containers)\n\t\t\tcontainerStatsRecord.Set(\"type\", \"1m\")\n\t\t\tif err := txApp.SaveNoValidate(containerStatsRecord); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// add new systemd_stats record\n\t\tif len(data.SystemdServices) > 0 {\n\t\t\tif err := createSystemdStatsRecords(txApp, data.SystemdServices, sys.Id); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// add system details record\n\t\tif data.Details != nil {\n\t\t\tif err := createSystemDetailsRecord(txApp, data.Details, sys.Id); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// update system record (do this last because it triggers alerts and we need above records to be inserted first)\n\t\tsystemRecord.Set(\"status\", up)\n\t\tsystemRecord.Set(\"info\", data.Info)\n\t\tif err := txApp.SaveNoValidate(systemRecord); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn systemRecord, err\n}\n\nfunc createSystemDetailsRecord(app core.App, data *system.Details, systemId string) error {\n\tcollectionName := \"system_details\"\n\tparams := dbx.Params{\n\t\t\"id\":       systemId,\n\t\t\"system\":   systemId,\n\t\t\"hostname\": data.Hostname,\n\t\t\"kernel\":   data.Kernel,\n\t\t\"cores\":    data.Cores,\n\t\t\"threads\":  data.Threads,\n\t\t\"cpu\":      data.CpuModel,\n\t\t\"os\":       data.Os,\n\t\t\"os_name\":  data.OsName,\n\t\t\"arch\":     data.Arch,\n\t\t\"memory\":   data.MemoryTotal,\n\t\t\"podman\":   data.Podman,\n\t\t\"updated\":  time.Now().UTC(),\n\t}\n\tresult, err := app.DB().Update(collectionName, params, dbx.HashExp{\"id\": systemId}).Execute()\n\trowsAffected, _ := result.RowsAffected()\n\tif err != nil || rowsAffected == 0 {\n\t\t_, err = app.DB().Insert(collectionName, params).Execute()\n\t}\n\treturn err\n}\n\nfunc createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\t// shared params for all records\n\tparams := dbx.Params{\n\t\t\"system\":  systemId,\n\t\t\"updated\": time.Now().UTC().UnixMilli(),\n\t}\n\n\tvalueStrings := make([]string, 0, len(data))\n\tfor i, service := range data {\n\t\tsuffix := fmt.Sprintf(\"%d\", i)\n\t\tvalueStrings = append(valueStrings, fmt.Sprintf(\"({:id%[1]s}, {:system}, {:name%[1]s}, {:state%[1]s}, {:sub%[1]s}, {:cpu%[1]s}, {:cpuPeak%[1]s}, {:memory%[1]s}, {:memPeak%[1]s}, {:updated})\", suffix))\n\t\tparams[\"id\"+suffix] = makeStableHashId(systemId, service.Name)\n\t\tparams[\"name\"+suffix] = service.Name\n\t\tparams[\"state\"+suffix] = service.State\n\t\tparams[\"sub\"+suffix] = service.Sub\n\t\tparams[\"cpu\"+suffix] = service.Cpu\n\t\tparams[\"cpuPeak\"+suffix] = service.CpuPeak\n\t\tparams[\"memory\"+suffix] = service.Mem\n\t\tparams[\"memPeak\"+suffix] = service.MemPeak\n\t}\n\tqueryString := fmt.Sprintf(\n\t\t\"INSERT INTO systemd_services (id, system, name, state, sub, cpu, cpuPeak, memory, memPeak, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system = excluded.system, name = excluded.name, state = excluded.state, sub = excluded.sub, cpu = excluded.cpu, cpuPeak = excluded.cpuPeak, memory = excluded.memory, memPeak = excluded.memPeak, updated = excluded.updated\",\n\t\tstrings.Join(valueStrings, \",\"),\n\t)\n\t_, err := app.DB().NewQuery(queryString).Bind(params).Execute()\n\treturn err\n}\n\n// createContainerRecords creates container records\nfunc createContainerRecords(app core.App, data []*container.Stats, systemId string) error {\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\t// shared params for all records\n\tparams := dbx.Params{\n\t\t\"system\":  systemId,\n\t\t\"updated\": time.Now().UTC().UnixMilli(),\n\t}\n\tvalueStrings := make([]string, 0, len(data))\n\tfor i, container := range data {\n\t\tsuffix := fmt.Sprintf(\"%d\", i)\n\t\tvalueStrings = append(valueStrings, fmt.Sprintf(\"({:id%[1]s}, {:system}, {:name%[1]s}, {:image%[1]s}, {:ports%[1]s}, {:status%[1]s}, {:health%[1]s}, {:cpu%[1]s}, {:memory%[1]s}, {:net%[1]s}, {:updated})\", suffix))\n\t\tparams[\"id\"+suffix] = container.Id\n\t\tparams[\"name\"+suffix] = container.Name\n\t\tparams[\"image\"+suffix] = container.Image\n\t\tparams[\"ports\"+suffix] = container.Ports\n\t\tparams[\"status\"+suffix] = container.Status\n\t\tparams[\"health\"+suffix] = container.Health\n\t\tparams[\"cpu\"+suffix] = container.Cpu\n\t\tparams[\"memory\"+suffix] = container.Mem\n\t\tnetBytes := container.Bandwidth[0] + container.Bandwidth[1]\n\t\tif netBytes == 0 {\n\t\t\tnetBytes = uint64((container.NetworkSent + container.NetworkRecv) * 1024 * 1024)\n\t\t}\n\t\tparams[\"net\"+suffix] = netBytes\n\t}\n\tqueryString := fmt.Sprintf(\n\t\t\"INSERT INTO containers (id, system, name, image, ports, status, health, cpu, memory, net, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system = excluded.system, name = excluded.name, image = excluded.image, ports = excluded.ports, status = excluded.status, health = excluded.health, cpu = excluded.cpu, memory = excluded.memory, net = excluded.net, updated = excluded.updated\",\n\t\tstrings.Join(valueStrings, \",\"),\n\t)\n\t_, err := app.DB().NewQuery(queryString).Bind(params).Execute()\n\treturn err\n}\n\n// getRecord retrieves the system record from the database.\n// If the record is not found, it removes the system from the manager.\nfunc (sys *System) getRecord() (*core.Record, error) {\n\trecord, err := sys.manager.hub.FindRecordById(\"systems\", sys.Id)\n\tif err != nil || record == nil {\n\t\t_ = sys.manager.RemoveSystem(sys.Id)\n\t\treturn nil, err\n\t}\n\treturn record, nil\n}\n\n// setDown marks a system as down in the database.\n// It takes the original error that caused the system to go down and returns any error\n// encountered during the process of updating the system status.\nfunc (sys *System) setDown(originalError error) error {\n\tif sys.Status == down || sys.Status == paused {\n\t\treturn nil\n\t}\n\trecord, err := sys.getRecord()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif originalError != nil {\n\t\tsys.manager.hub.Logger().Error(\"System down\", \"system\", record.GetString(\"name\"), \"err\", originalError)\n\t}\n\trecord.Set(\"status\", down)\n\treturn sys.manager.hub.SaveNoValidate(record)\n}\n\nfunc (sys *System) getContext() (context.Context, context.CancelFunc) {\n\tif sys.ctx == nil {\n\t\tsys.ctx, sys.cancel = context.WithCancel(context.Background())\n\t}\n\treturn sys.ctx, sys.cancel\n}\n\n// request sends a request to the agent, trying WebSocket first, then SSH.\n// This is the unified request method that uses the transport abstraction.\nfunc (sys *System) request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {\n\t// Try WebSocket first\n\tif sys.WsConn != nil && sys.WsConn.IsConnected() {\n\t\twsTransport := transport.NewWebSocketTransport(sys.WsConn)\n\t\tif err := wsTransport.Request(ctx, action, req, dest); err == nil {\n\t\t\treturn nil\n\t\t} else if !shouldFallbackToSSH(err) {\n\t\t\treturn err\n\t\t} else if shouldCloseWebSocket(err) {\n\t\t\tsys.closeWebSocketConnection()\n\t\t}\n\t}\n\n\t// Fall back to SSH if WebSocket fails\n\tif err := sys.ensureSSHTransport(); err != nil {\n\t\treturn err\n\t}\n\terr := sys.sshTransport.RequestWithRetry(ctx, action, req, dest, 1)\n\t// Keep legacy SSH client/version fields in sync for other code paths.\n\tif sys.sshTransport != nil {\n\t\tsys.client = sys.sshTransport.GetClient()\n\t\tsys.agentVersion = sys.sshTransport.GetAgentVersion()\n\t}\n\treturn err\n}\n\nfunc shouldFallbackToSSH(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tif errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {\n\t\treturn true\n\t}\n\tif errors.Is(err, gws.ErrConnClosed) {\n\t\treturn true\n\t}\n\treturn errors.Is(err, transport.ErrWebSocketNotConnected)\n}\n\nfunc shouldCloseWebSocket(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn errors.Is(err, gws.ErrConnClosed) || errors.Is(err, transport.ErrWebSocketNotConnected)\n}\n\n// ensureSSHTransport ensures the SSH transport is initialized and connected.\nfunc (sys *System) ensureSSHTransport() error {\n\tif sys.sshTransport == nil {\n\t\tif sys.manager.sshConfig == nil {\n\t\t\tif err := sys.manager.createSSHClientConfig(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tsys.sshTransport = transport.NewSSHTransport(transport.SSHTransportConfig{\n\t\t\tHost:    sys.Host,\n\t\t\tPort:    sys.Port,\n\t\t\tConfig:  sys.manager.sshConfig,\n\t\t\tTimeout: 4 * time.Second,\n\t\t})\n\t}\n\t// Sync client state with transport\n\tif sys.client != nil {\n\t\tsys.sshTransport.SetClient(sys.client)\n\t\tsys.sshTransport.SetAgentVersion(sys.agentVersion)\n\t}\n\treturn nil\n}\n\n// fetchDataFromAgent attempts to fetch data from the agent, prioritizing WebSocket if available.\nfunc (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*system.CombinedData, error) {\n\tif sys.data == nil {\n\t\tsys.data = &system.CombinedData{}\n\t}\n\n\tif sys.WsConn != nil && sys.WsConn.IsConnected() {\n\t\twsData, err := sys.fetchDataViaWebSocket(options)\n\t\tif err == nil {\n\t\t\treturn wsData, nil\n\t\t}\n\t\t// close the WebSocket connection if error and try SSH\n\t\tsys.closeWebSocketConnection()\n\t}\n\n\tsshData, err := sys.fetchDataViaSSH(options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sshData, nil\n}\n\nfunc (sys *System) fetchDataViaWebSocket(options common.DataRequestOptions) (*system.CombinedData, error) {\n\tif sys.WsConn == nil || !sys.WsConn.IsConnected() {\n\t\treturn nil, errors.New(\"no websocket connection\")\n\t}\n\twsTransport := transport.NewWebSocketTransport(sys.WsConn)\n\terr := wsTransport.Request(context.Background(), common.GetData, options, sys.data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sys.data, nil\n}\n\n// FetchContainerInfoFromAgent fetches container info from the agent\nfunc (sys *System) FetchContainerInfoFromAgent(containerID string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tvar result string\n\terr := sys.request(ctx, common.GetContainerInfo, common.ContainerInfoRequest{ContainerID: containerID}, &result)\n\treturn result, err\n}\n\n// FetchContainerLogsFromAgent fetches container logs from the agent\nfunc (sys *System) FetchContainerLogsFromAgent(containerID string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tvar result string\n\terr := sys.request(ctx, common.GetContainerLogs, common.ContainerLogsRequest{ContainerID: containerID}, &result)\n\treturn result, err\n}\n\n// FetchSystemdInfoFromAgent fetches detailed systemd service information from the agent\nfunc (sys *System) FetchSystemdInfoFromAgent(serviceName string) (systemd.ServiceDetails, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tvar result systemd.ServiceDetails\n\terr := sys.request(ctx, common.GetSystemdInfo, common.SystemdInfoRequest{ServiceName: serviceName}, &result)\n\treturn result, err\n}\n\n// FetchSmartDataFromAgent fetches SMART data from the agent\nfunc (sys *System) FetchSmartDataFromAgent() (map[string]smart.SmartData, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)\n\tdefer cancel()\n\tvar result map[string]smart.SmartData\n\terr := sys.request(ctx, common.GetSmartData, nil, &result)\n\treturn result, err\n}\n\nfunc makeStableHashId(strings ...string) string {\n\thash := fnv.New32a()\n\tfor _, str := range strings {\n\t\thash.Write([]byte(str))\n\t}\n\treturn fmt.Sprintf(\"%x\", hash.Sum32())\n}\n\n// fetchDataViaSSH handles fetching data using SSH.\n// This function encapsulates the original SSH logic.\n// It updates sys.data directly upon successful fetch.\nfunc (sys *System) fetchDataViaSSH(options common.DataRequestOptions) (*system.CombinedData, error) {\n\terr := sys.runSSHOperation(4*time.Second, 1, func(session *ssh.Session) (bool, error) {\n\t\tstdout, err := session.StdoutPipe()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tstdin, stdinErr := session.StdinPipe()\n\t\tif err := session.Shell(); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\t*sys.data = system.CombinedData{}\n\n\t\tif sys.agentVersion.GTE(beszel.MinVersionAgentResponse) && stdinErr == nil {\n\t\t\treq := common.HubRequest[any]{Action: common.GetData, Data: options}\n\t\t\t_ = cbor.NewEncoder(stdin).Encode(req)\n\t\t\t_ = stdin.Close()\n\n\t\t\tvar resp common.AgentResponse\n\t\t\tif decErr := cbor.NewDecoder(stdout).Decode(&resp); decErr == nil && resp.SystemData != nil {\n\t\t\t\t*sys.data = *resp.SystemData\n\t\t\t\tif err := session.Wait(); err != nil {\n\t\t\t\t\treturn false, err\n\t\t\t\t}\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\n\t\tvar decodeErr error\n\t\tif sys.agentVersion.GTE(beszel.MinVersionCbor) {\n\t\t\tdecodeErr = cbor.NewDecoder(stdout).Decode(sys.data)\n\t\t} else {\n\t\t\tdecodeErr = json.NewDecoder(stdout).Decode(sys.data)\n\t\t}\n\n\t\tif decodeErr != nil {\n\t\t\treturn true, decodeErr\n\t\t}\n\n\t\tif err := session.Wait(); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\treturn false, nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn sys.data, nil\n}\n\n// runSSHOperation establishes an SSH session and executes the provided operation.\n// The operation can request a retry by returning true as the first return value.\nfunc (sys *System) runSSHOperation(timeout time.Duration, retries int, operation func(*ssh.Session) (bool, error)) error {\n\tfor attempt := 0; attempt <= retries; attempt++ {\n\t\tif sys.client == nil || sys.Status == down {\n\t\t\tif err := sys.createSSHClient(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tsession, err := sys.createSessionWithTimeout(timeout)\n\t\tif err != nil {\n\t\t\tif attempt >= retries {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsys.manager.hub.Logger().Warn(\"Session closed. Retrying...\", \"host\", sys.Host, \"port\", sys.Port, \"err\", err)\n\t\t\tsys.closeSSHConnection()\n\t\t\tcontinue\n\t\t}\n\n\t\tretry, opErr := func() (bool, error) {\n\t\t\tdefer session.Close()\n\t\t\treturn operation(session)\n\t\t}()\n\n\t\tif opErr == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif retry {\n\t\t\tsys.closeSSHConnection()\n\t\t\tif attempt < retries {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn opErr\n\t}\n\n\treturn fmt.Errorf(\"ssh operation failed\")\n}\n\n// createSSHClient creates a new SSH client for the system\nfunc (s *System) createSSHClient() error {\n\tif s.manager.sshConfig == nil {\n\t\tif err := s.manager.createSSHClientConfig(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tnetwork := \"tcp\"\n\thost := s.Host\n\tif strings.HasPrefix(host, \"/\") {\n\t\tnetwork = \"unix\"\n\t} else {\n\t\thost = net.JoinHostPort(host, s.Port)\n\t}\n\tvar err error\n\ts.client, err = ssh.Dial(network, host, s.manager.sshConfig)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.agentVersion, _ = extractAgentVersion(string(s.client.Conn.ServerVersion()))\n\treturn nil\n}\n\n// createSessionWithTimeout creates a new SSH session with a timeout to avoid hanging\n// in case of network issues\nfunc (sys *System) createSessionWithTimeout(timeout time.Duration) (*ssh.Session, error) {\n\tif sys.client == nil {\n\t\treturn nil, fmt.Errorf(\"client not initialized\")\n\t}\n\n\tctx, cancel := context.WithTimeout(sys.ctx, timeout)\n\tdefer cancel()\n\n\tsessionChan := make(chan *ssh.Session, 1)\n\terrChan := make(chan error, 1)\n\n\tgo func() {\n\t\tif session, err := sys.client.NewSession(); err != nil {\n\t\t\terrChan <- err\n\t\t} else {\n\t\t\tsessionChan <- session\n\t\t}\n\t}()\n\n\tselect {\n\tcase session := <-sessionChan:\n\t\treturn session, nil\n\tcase err := <-errChan:\n\t\treturn nil, err\n\tcase <-ctx.Done():\n\t\treturn nil, fmt.Errorf(\"timeout\")\n\t}\n}\n\n// closeSSHConnection closes the SSH connection but keeps the system in the manager\nfunc (sys *System) closeSSHConnection() {\n\tif sys.sshTransport != nil {\n\t\tsys.sshTransport.Close()\n\t}\n\tif sys.client != nil {\n\t\tsys.client.Close()\n\t\tsys.client = nil\n\t}\n}\n\n// closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager\n// to allow updating via SSH. It will be removed if the WS connection is re-established.\n// The system will be set as down a few seconds later if the connection is not re-established.\nfunc (sys *System) closeWebSocketConnection() {\n\tif sys.WsConn != nil {\n\t\tsys.WsConn.Close(nil)\n\t}\n}\n\n// extractAgentVersion extracts the beszel version from SSH server version string\nfunc extractAgentVersion(versionString string) (semver.Version, error) {\n\t_, after, _ := strings.Cut(versionString, \"_\")\n\treturn semver.Parse(after)\n}\n\n// getJitter returns a channel that will be triggered after a random delay\n// between 51% and 95% of the interval.\n// This is used to stagger the initial WebSocket connections to prevent clustering.\nfunc getJitter() <-chan time.Time {\n\tminPercent := 51\n\tmaxPercent := 95\n\tjitterRange := maxPercent - minPercent\n\tmsDelay := (interval * minPercent / 100) + rand.Intn(interval*jitterRange/100)\n\treturn time.After(time.Duration(msDelay) * time.Millisecond)\n}\n\n// migrateDeprecatedFields moves values from deprecated fields to their new locations if the new\n// fields are not already populated. Deprecated fields and refs may be removed at least 30 days\n// and one minor version release after the release that includes the migration.\n//\n// This is run when processing incoming system data from agents, which may be on older versions.\nfunc migrateDeprecatedFields(cd *system.CombinedData, createDetails bool) {\n\t// migration added 0.19.0\n\tif cd.Stats.Bandwidth[0] == 0 && cd.Stats.Bandwidth[1] == 0 {\n\t\tcd.Stats.Bandwidth[0] = uint64(cd.Stats.NetworkSent * 1024 * 1024)\n\t\tcd.Stats.Bandwidth[1] = uint64(cd.Stats.NetworkRecv * 1024 * 1024)\n\t\tcd.Stats.NetworkSent, cd.Stats.NetworkRecv = 0, 0\n\t}\n\t// migration added 0.19.0\n\tif cd.Info.BandwidthBytes == 0 {\n\t\tcd.Info.BandwidthBytes = uint64(cd.Info.Bandwidth * 1024 * 1024)\n\t\tcd.Info.Bandwidth = 0\n\t}\n\t// migration added 0.19.0\n\tif cd.Stats.DiskIO[0] == 0 && cd.Stats.DiskIO[1] == 0 {\n\t\tcd.Stats.DiskIO[0] = uint64(cd.Stats.DiskReadPs * 1024 * 1024)\n\t\tcd.Stats.DiskIO[1] = uint64(cd.Stats.DiskWritePs * 1024 * 1024)\n\t\tcd.Stats.DiskReadPs, cd.Stats.DiskWritePs = 0, 0\n\t}\n\t// migration added 0.19.0 - Move deprecated Info fields to Details struct\n\tif cd.Details == nil && cd.Info.Hostname != \"\" {\n\t\tif createDetails {\n\t\t\tcd.Details = &system.Details{\n\t\t\t\tHostname:    cd.Info.Hostname,\n\t\t\t\tKernel:      cd.Info.KernelVersion,\n\t\t\t\tCores:       cd.Info.Cores,\n\t\t\t\tThreads:     cd.Info.Threads,\n\t\t\t\tCpuModel:    cd.Info.CpuModel,\n\t\t\t\tPodman:      cd.Info.Podman,\n\t\t\t\tOs:          cd.Info.Os,\n\t\t\t\tMemoryTotal: uint64(cd.Stats.Mem * 1024 * 1024 * 1024),\n\t\t\t}\n\t\t}\n\t\t// zero the deprecated fields to prevent saving them in systems.info DB json payload\n\t\tcd.Info.Hostname = \"\"\n\t\tcd.Info.KernelVersion = \"\"\n\t\tcd.Info.Cores = 0\n\t\tcd.Info.CpuModel = \"\"\n\t\tcd.Info.Podman = false\n\t\tcd.Info.Os = 0\n\t}\n}\n"
  },
  {
    "path": "internal/hub/systems/system_manager.go",
    "content": "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/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/hub/expirymap\"\n\n\t\"github.com/henrygd/beszel/internal/common\"\n\n\t\"github.com/henrygd/beszel\"\n\n\t\"github.com/blang/semver\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/store\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// System status constants\nconst (\n\tup      string = \"up\"      // System is online and responding\n\tdown    string = \"down\"    // System is offline or not responding\n\tpaused  string = \"paused\"  // System monitoring is paused\n\tpending string = \"pending\" // System is waiting on initial connection result\n\n\t// interval is the default update interval in milliseconds (60 seconds)\n\tinterval int = 60_000\n\t// interval int = 10_000 // Debug interval for faster updates\n\n\t// sessionTimeout is the maximum time to wait for SSH connections\n\tsessionTimeout = 4 * time.Second\n)\n\n// errSystemExists is returned when attempting to add a system that already exists\nvar errSystemExists = errors.New(\"system exists\")\n\n// SystemManager manages a collection of monitored systems and their connections.\n// It handles system lifecycle, status updates, and maintains both SSH and WebSocket connections.\ntype SystemManager struct {\n\thub           hubLike                       // Hub interface for database and alert operations\n\tsystems       *store.Store[string, *System] // Thread-safe store of active systems\n\tsshConfig     *ssh.ClientConfig             // SSH client configuration for system connections\n\tsmartFetchMap *expirymap.ExpiryMap[int64]   // Stores last SMART fetch time per system ID\n}\n\n// hubLike defines the interface requirements for the hub dependency.\n// It extends core.App with system-specific functionality.\ntype hubLike interface {\n\tcore.App\n\tGetSSHKey(dataDir string) (ssh.Signer, error)\n\tHandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error\n\tHandleStatusAlerts(status string, systemRecord *core.Record) error\n}\n\n// NewSystemManager creates a new SystemManager instance with the provided hub.\n// The hub must implement the hubLike interface to provide database and alert functionality.\nfunc NewSystemManager(hub hubLike) *SystemManager {\n\treturn &SystemManager{\n\t\tsystems:       store.New(map[string]*System{}),\n\t\thub:           hub,\n\t\tsmartFetchMap: expirymap.New[int64](time.Hour),\n\t}\n}\n\n// GetSystem returns a system by ID from the store\nfunc (sm *SystemManager) GetSystem(systemID string) (*System, error) {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"system not found\")\n\t}\n\treturn sys, nil\n}\n\n// Initialize sets up the system manager by binding event hooks and starting existing systems.\n// It configures SSH client settings and begins monitoring all non-paused systems from the database.\n// Systems are started with staggered delays to prevent overwhelming the hub during startup.\nfunc (sm *SystemManager) Initialize() error {\n\tsm.bindEventHooks()\n\n\t// Initialize SSH client configuration\n\terr := sm.createSSHClientConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Load existing systems from database (excluding paused ones)\n\tvar systems []*System\n\terr = sm.hub.DB().NewQuery(\"SELECT id, host, port, status FROM systems WHERE status != 'paused'\").All(&systems)\n\tif err != nil || len(systems) == 0 {\n\t\treturn err\n\t}\n\n\t// Start systems in background with staggered timing\n\tgo func() {\n\t\t// Calculate staggered delay between system starts (max 2 seconds per system)\n\t\tdelta := interval / max(1, len(systems))\n\t\tdelta = min(delta, 2_000)\n\t\tsleepTime := time.Duration(delta) * time.Millisecond\n\n\t\tfor _, system := range systems {\n\t\t\ttime.Sleep(sleepTime)\n\t\t\t_ = sm.AddSystem(system)\n\t\t}\n\t}()\n\treturn nil\n}\n\n// bindEventHooks registers event handlers for system and fingerprint record changes.\n// These hooks ensure the system manager stays synchronized with database changes.\nfunc (sm *SystemManager) bindEventHooks() {\n\tsm.hub.OnRecordCreate(\"systems\").BindFunc(sm.onRecordCreate)\n\tsm.hub.OnRecordAfterCreateSuccess(\"systems\").BindFunc(sm.onRecordAfterCreateSuccess)\n\tsm.hub.OnRecordUpdate(\"systems\").BindFunc(sm.onRecordUpdate)\n\tsm.hub.OnRecordAfterUpdateSuccess(\"systems\").BindFunc(sm.onRecordAfterUpdateSuccess)\n\tsm.hub.OnRecordAfterDeleteSuccess(\"systems\").BindFunc(sm.onRecordAfterDeleteSuccess)\n\tsm.hub.OnRecordAfterUpdateSuccess(\"fingerprints\").BindFunc(sm.onTokenRotated)\n\tsm.hub.OnRealtimeSubscribeRequest().BindFunc(sm.onRealtimeSubscribeRequest)\n\tsm.hub.OnRealtimeConnectRequest().BindFunc(sm.onRealtimeConnectRequest)\n}\n\n// onTokenRotated handles fingerprint token rotation events.\n// When a system's authentication token is rotated, any existing WebSocket connection\n// must be closed to force re-authentication with the new token.\nfunc (sm *SystemManager) onTokenRotated(e *core.RecordEvent) error {\n\tsystemID := e.Record.GetString(\"system\")\n\tsystem, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn e.Next()\n\t}\n\t// No need to close connection if not connected via websocket\n\tif system.WsConn == nil {\n\t\treturn e.Next()\n\t}\n\tsystem.setDown(nil)\n\tsm.RemoveSystem(systemID)\n\treturn e.Next()\n}\n\n// onRecordCreate is called before a new system record is committed to the database.\n// It initializes the record with default values: empty info and pending status.\nfunc (sm *SystemManager) onRecordCreate(e *core.RecordEvent) error {\n\te.Record.Set(\"info\", system.Info{})\n\te.Record.Set(\"status\", pending)\n\treturn e.Next()\n}\n\n// onRecordAfterCreateSuccess is called after a new system record is successfully created.\n// It adds the new system to the manager to begin monitoring.\nfunc (sm *SystemManager) onRecordAfterCreateSuccess(e *core.RecordEvent) error {\n\tif err := sm.AddRecord(e.Record, nil); err != nil {\n\t\te.App.Logger().Error(\"Error adding record\", \"err\", err)\n\t}\n\treturn e.Next()\n}\n\n// onRecordUpdate is called before a system record is updated in the database.\n// It clears system info when the status is changed to paused.\nfunc (sm *SystemManager) onRecordUpdate(e *core.RecordEvent) error {\n\tif e.Record.GetString(\"status\") == paused {\n\t\te.Record.Set(\"info\", system.Info{})\n\t}\n\treturn e.Next()\n}\n\n// onRecordAfterUpdateSuccess handles system record updates after they're committed to the database.\n// It manages system lifecycle based on status changes and triggers appropriate alerts.\n// Status transitions are handled as follows:\n// - paused: Closes SSH connection and deactivates alerts\n// - pending: Starts monitoring (reuses WebSocket if available)\n// - up: Triggers system alerts\n// - down: Triggers status change alerts\nfunc (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {\n\tnewStatus := e.Record.GetString(\"status\")\n\tprevStatus := pending\n\tsystem, ok := sm.systems.GetOk(e.Record.Id)\n\tif ok {\n\t\tprevStatus = system.Status\n\t\tsystem.Status = newStatus\n\t}\n\n\tswitch newStatus {\n\tcase paused:\n\t\tif ok {\n\t\t\t// Pause monitoring but keep system in manager for potential resume\n\t\t\tsystem.closeSSHConnection()\n\t\t}\n\t\t_ = deactivateAlerts(e.App, e.Record.Id)\n\t\treturn e.Next()\n\tcase pending:\n\t\t// Resume monitoring, preferring existing WebSocket connection\n\t\tif ok && system.WsConn != nil {\n\t\t\tgo system.update()\n\t\t\treturn e.Next()\n\t\t}\n\t\t// Start new monitoring session\n\t\tif err := sm.AddRecord(e.Record, nil); err != nil {\n\t\t\te.App.Logger().Error(\"Error adding record\", \"err\", err)\n\t\t}\n\t\t_ = deactivateAlerts(e.App, e.Record.Id)\n\t\treturn e.Next()\n\t}\n\n\t// Handle systems not in manager\n\tif !ok {\n\t\treturn sm.AddRecord(e.Record, nil)\n\t}\n\n\t// Trigger system alerts when system comes online\n\tif newStatus == up {\n\t\tif err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil {\n\t\t\te.App.Logger().Error(\"Error handling system alerts\", \"err\", err)\n\t\t}\n\t}\n\n\t// Trigger status change alerts for up/down transitions\n\tif (newStatus == down && prevStatus == up) || (newStatus == up && prevStatus == down) {\n\t\tif err := sm.hub.HandleStatusAlerts(newStatus, e.Record); err != nil {\n\t\t\te.App.Logger().Error(\"Error handling status alerts\", \"err\", err)\n\t\t}\n\t}\n\treturn e.Next()\n}\n\n// onRecordAfterDeleteSuccess is called after a system record is successfully deleted.\n// It removes the system from the manager and cleans up all associated resources.\nfunc (sm *SystemManager) onRecordAfterDeleteSuccess(e *core.RecordEvent) error {\n\tsm.RemoveSystem(e.Record.Id)\n\treturn e.Next()\n}\n\n// AddSystem adds a system to the manager and starts monitoring it.\n// It validates required fields, initializes the system context, and starts the update goroutine.\n// Returns error if a system with the same ID already exists.\nfunc (sm *SystemManager) AddSystem(sys *System) error {\n\tif sm.systems.Has(sys.Id) {\n\t\treturn errSystemExists\n\t}\n\tif sys.Id == \"\" || sys.Host == \"\" {\n\t\treturn errors.New(\"system missing required fields\")\n\t}\n\n\t// Initialize system for monitoring\n\tsys.manager = sm\n\tsys.ctx, sys.cancel = sys.getContext()\n\tsys.data = &system.CombinedData{}\n\tsm.systems.Set(sys.Id, sys)\n\n\t// Start monitoring in background\n\tgo sys.StartUpdater()\n\treturn nil\n}\n\n// RemoveSystem removes a system from the manager and cleans up all associated resources.\n// It cancels the system's context, closes all connections, and removes it from the store.\n// Returns an error if the system is not found.\nfunc (sm *SystemManager) RemoveSystem(systemID string) error {\n\tsystem, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn errors.New(\"system not found\")\n\t}\n\n\t// Stop the update goroutine\n\tif system.cancel != nil {\n\t\tsystem.cancel()\n\t}\n\n\t// Clean up all connections\n\tsystem.closeSSHConnection()\n\tsystem.closeWebSocketConnection()\n\tsm.systems.Remove(systemID)\n\treturn nil\n}\n\n// AddRecord creates a System instance from a database record and adds it to the manager.\n// If a system with the same ID already exists, it's removed first to ensure clean state.\n// If no system instance is provided, a new one is created.\n// This method is typically called when systems are created or their status changes to pending.\nfunc (sm *SystemManager) AddRecord(record *core.Record, system *System) (err error) {\n\t// Remove existing system to ensure clean state\n\tif sm.systems.Has(record.Id) {\n\t\t_ = sm.RemoveSystem(record.Id)\n\t}\n\n\t// Create new system if none provided\n\tif system == nil {\n\t\tsystem = sm.NewSystem(record.Id)\n\t}\n\n\t// Populate system from record\n\tsystem.Status = record.GetString(\"status\")\n\tsystem.Host = record.GetString(\"host\")\n\tsystem.Port = record.GetString(\"port\")\n\n\treturn sm.AddSystem(system)\n}\n\n// AddWebSocketSystem creates and adds a system with an established WebSocket connection.\n// This method is called when an agent connects via WebSocket with valid authentication.\n// The system is immediately added to monitoring with the provided connection and version info.\nfunc (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver.Version, wsConn *ws.WsConn) error {\n\tsystemRecord, err := sm.hub.FindRecordById(\"systems\", systemId)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsystem := sm.NewSystem(systemId)\n\tsystem.WsConn = wsConn\n\tsystem.agentVersion = agentVersion\n\n\tif err := sm.AddRecord(systemRecord, system); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// createSSHClientConfig initializes the SSH client configuration for connecting to an agent's server\nfunc (sm *SystemManager) createSSHClientConfig() error {\n\tprivateKey, err := sm.hub.GetSSHKey(\"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsm.sshConfig = &ssh.ClientConfig{\n\t\tUser: \"u\",\n\t\tAuth: []ssh.AuthMethod{\n\t\t\tssh.PublicKeys(privateKey),\n\t\t},\n\t\tConfig: ssh.Config{\n\t\t\tCiphers:      common.DefaultCiphers,\n\t\t\tKeyExchanges: common.DefaultKeyExchanges,\n\t\t\tMACs:         common.DefaultMACs,\n\t\t},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tClientVersion:   fmt.Sprintf(\"SSH-2.0-%s_%s\", beszel.AppName, beszel.Version),\n\t\tTimeout:         sessionTimeout,\n\t}\n\treturn nil\n}\n\n// deactivateAlerts finds all triggered alerts for a system and sets them to inactive.\n// This is called when a system is paused or goes offline to prevent continued alerts.\nfunc deactivateAlerts(app core.App, systemID string) error {\n\t// Note: Direct SQL updates don't trigger SSE, so we use the PocketBase API\n\t// _, err := app.DB().NewQuery(fmt.Sprintf(\"UPDATE alerts SET triggered = false WHERE system = '%s'\", systemID)).Execute()\n\n\talerts, err := app.FindRecordsByFilter(\"alerts\", fmt.Sprintf(\"system = '%s' && triggered = 1\", systemID), \"\", -1, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, alert := range alerts {\n\t\talert.Set(\"triggered\", false)\n\t\tif err := app.SaveNoValidate(alert); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/hub/systems/system_realtime.go",
    "content": "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\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/subscriptions\"\n)\n\ntype subscriptionInfo struct {\n\tsubscription     string\n\tconnectedClients uint8\n}\n\nvar (\n\tactiveSubscriptions = make(map[string]*subscriptionInfo)\n\tworkerRunning       bool\n\trealtimeTicker      *time.Ticker\n\ttickerStopChan      chan struct{}\n\trealtimeMutex       sync.Mutex\n)\n\n// onRealtimeConnectRequest handles client connection events for realtime subscriptions.\n// It cleans up existing subscriptions when a client connects.\nfunc (sm *SystemManager) onRealtimeConnectRequest(e *core.RealtimeConnectRequestEvent) error {\n\t// after e.Next() is the client disconnection\n\te.Next()\n\tsubscriptions := e.Client.Subscriptions()\n\tfor k := range subscriptions {\n\t\tsm.removeRealtimeSubscription(k, subscriptions[k])\n\t}\n\treturn nil\n}\n\n// onRealtimeSubscribeRequest handles client subscription events for realtime metrics.\n// It tracks new subscriptions and unsubscriptions to manage the realtime worker lifecycle.\nfunc (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeRequestEvent) error {\n\toldSubs := e.Client.Subscriptions()\n\t// after e.Next() is the result of the subscribe request\n\terr := e.Next()\n\tnewSubs := e.Client.Subscriptions()\n\n\t// handle new subscriptions\n\tfor k, options := range newSubs {\n\t\tif _, ok := oldSubs[k]; !ok {\n\t\t\tif strings.HasPrefix(k, \"rt_metrics\") {\n\t\t\t\tsystemId := options.Query[\"system\"]\n\t\t\t\tif _, ok := activeSubscriptions[systemId]; !ok {\n\t\t\t\t\tactiveSubscriptions[systemId] = &subscriptionInfo{\n\t\t\t\t\t\tsubscription: k,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tactiveSubscriptions[systemId].connectedClients += 1\n\t\t\t\tsm.onRealtimeSubscriptionAdded()\n\t\t\t}\n\t\t}\n\t}\n\t// handle unsubscriptions\n\tfor k := range oldSubs {\n\t\tif _, ok := newSubs[k]; !ok {\n\t\t\tsm.removeRealtimeSubscription(k, oldSubs[k])\n\t\t}\n\t}\n\n\treturn err\n}\n\n// onRealtimeSubscriptionAdded initializes or starts the realtime worker when the first subscription is added.\n// It ensures only one worker runs at a time and creates the ticker for periodic data fetching.\nfunc (sm *SystemManager) onRealtimeSubscriptionAdded() {\n\trealtimeMutex.Lock()\n\tdefer realtimeMutex.Unlock()\n\n\t// Start the worker if it's not already running\n\tif !workerRunning {\n\t\tworkerRunning = true\n\t\t// Create a new stop channel for this worker instance\n\t\ttickerStopChan = make(chan struct{})\n\t\tgo sm.startRealtimeWorker()\n\t}\n\n\t// If no ticker exists, create one\n\tif realtimeTicker == nil {\n\t\trealtimeTicker = time.NewTicker(1 * time.Second)\n\t}\n}\n\n// checkSubscriptions stops the realtime worker when there are no active subscriptions.\n// This prevents unnecessary resource usage when no clients are listening for realtime data.\nfunc (sm *SystemManager) checkSubscriptions() {\n\tif !workerRunning || len(activeSubscriptions) > 0 {\n\t\treturn\n\t}\n\n\trealtimeMutex.Lock()\n\tdefer realtimeMutex.Unlock()\n\n\t// Signal the worker to stop\n\tif tickerStopChan != nil {\n\t\tselect {\n\t\tcase tickerStopChan <- struct{}{}:\n\t\tdefault:\n\t\t}\n\t}\n\n\tif realtimeTicker != nil {\n\t\trealtimeTicker.Stop()\n\t\trealtimeTicker = nil\n\t}\n\n\t// Mark worker as stopped (will be reset when next subscription comes in)\n\tworkerRunning = false\n}\n\n// removeRealtimeSubscription removes a realtime subscription and checks if the worker should be stopped.\n// It only processes subscriptions with the \"rt_metrics\" prefix and triggers cleanup when subscriptions are removed.\nfunc (sm *SystemManager) removeRealtimeSubscription(subscription string, options subscriptions.SubscriptionOptions) {\n\tif strings.HasPrefix(subscription, \"rt_metrics\") {\n\t\tsystemId := options.Query[\"system\"]\n\t\tif info, ok := activeSubscriptions[systemId]; ok {\n\t\t\tinfo.connectedClients -= 1\n\t\t\tif info.connectedClients <= 0 {\n\t\t\t\tdelete(activeSubscriptions, systemId)\n\t\t\t}\n\t\t}\n\t\tsm.checkSubscriptions()\n\t}\n}\n\n// startRealtimeWorker runs the main loop for fetching realtime data from agents.\n// It continuously fetches system data and broadcasts it to subscribed clients via WebSocket.\nfunc (sm *SystemManager) startRealtimeWorker() {\n\tsm.fetchRealtimeDataAndNotify()\n\n\tfor {\n\t\tselect {\n\t\tcase <-tickerStopChan:\n\t\t\treturn\n\t\tcase <-realtimeTicker.C:\n\t\t\t// Check if ticker is still valid (might have been stopped)\n\t\t\tif realtimeTicker == nil || len(activeSubscriptions) == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// slog.Debug(\"activeSubscriptions\", \"count\", len(activeSubscriptions))\n\t\t\tsm.fetchRealtimeDataAndNotify()\n\t\t}\n\t}\n}\n\n// fetchRealtimeDataAndNotify fetches realtime data for all active subscriptions and notifies the clients.\nfunc (sm *SystemManager) fetchRealtimeDataAndNotify() {\n\tfor systemId, info := range activeSubscriptions {\n\t\tsystem, err := sm.GetSystem(systemId)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tgo func() {\n\t\t\tdata, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbytes, err := json.Marshal(data)\n\t\t\tif err == nil {\n\t\t\t\tnotify(sm.hub, info.subscription, bytes)\n\t\t\t}\n\t\t}()\n\t}\n}\n\n// notify broadcasts realtime data to all clients subscribed to a specific subscription.\n// It iterates through all connected clients and sends the data only to those with matching subscriptions.\nfunc notify(app core.App, subscription string, data []byte) error {\n\tmessage := subscriptions.Message{\n\t\tName: subscription,\n\t\tData: data,\n\t}\n\tfor _, client := range app.SubscriptionsBroker().Clients() {\n\t\tif !client.HasSubscription(subscription) {\n\t\t\tcontinue\n\t\t}\n\t\tclient.Send(message)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/hub/systems/system_smart.go",
    "content": "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\"github.com/pocketbase/pocketbase/core\"\n)\n\n// FetchAndSaveSmartDevices fetches SMART data from the agent and saves it to the database\nfunc (sys *System) FetchAndSaveSmartDevices() error {\n\tsmartData, err := sys.FetchSmartDataFromAgent()\n\tif err != nil || len(smartData) == 0 {\n\t\treturn err\n\t}\n\treturn sys.saveSmartDevices(smartData)\n}\n\n// saveSmartDevices saves SMART device data to the smart_devices collection\nfunc (sys *System) saveSmartDevices(smartData map[string]smart.SmartData) error {\n\tif len(smartData) == 0 {\n\t\treturn nil\n\t}\n\n\thub := sys.manager.hub\n\tcollection, err := hub.FindCachedCollectionByNameOrId(\"smart_devices\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor deviceKey, device := range smartData {\n\t\tif err := sys.upsertSmartDeviceRecord(collection, deviceKey, device); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (sys *System) upsertSmartDeviceRecord(collection *core.Collection, deviceKey string, device smart.SmartData) error {\n\thub := sys.manager.hub\n\trecordID := makeStableHashId(sys.Id, deviceKey)\n\n\trecord, err := hub.FindRecordById(collection, recordID)\n\tif err != nil {\n\t\tif !errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn err\n\t\t}\n\t\trecord = core.NewRecord(collection)\n\t\trecord.Set(\"id\", recordID)\n\t}\n\n\tname := device.DiskName\n\tif name == \"\" {\n\t\tname = deviceKey\n\t}\n\n\tpowerOnHours, powerCycles := extractPowerMetrics(device.Attributes)\n\trecord.Set(\"system\", sys.Id)\n\trecord.Set(\"name\", name)\n\trecord.Set(\"model\", device.ModelName)\n\trecord.Set(\"state\", device.SmartStatus)\n\trecord.Set(\"capacity\", device.Capacity)\n\trecord.Set(\"temp\", device.Temperature)\n\trecord.Set(\"firmware\", device.FirmwareVersion)\n\trecord.Set(\"serial\", device.SerialNumber)\n\trecord.Set(\"type\", device.DiskType)\n\trecord.Set(\"hours\", powerOnHours)\n\trecord.Set(\"cycles\", powerCycles)\n\trecord.Set(\"attributes\", device.Attributes)\n\n\treturn hub.SaveNoValidate(record)\n}\n\n// extractPowerMetrics extracts power on hours and power cycles from SMART attributes\nfunc extractPowerMetrics(attributes []*smart.SmartAttribute) (powerOnHours, powerCycles uint64) {\n\tfor _, attr := range attributes {\n\t\tnameLower := strings.ToLower(attr.Name)\n\t\tif powerOnHours == 0 && (strings.Contains(nameLower, \"poweronhours\") || strings.Contains(nameLower, \"power_on_hours\")) {\n\t\t\tpowerOnHours = attr.RawValue\n\t\t}\n\t\tif powerCycles == 0 && ((strings.Contains(nameLower, \"power\") && strings.Contains(nameLower, \"cycle\")) || strings.Contains(nameLower, \"startstopcycles\")) {\n\t\t\tpowerCycles = attr.RawValue\n\t\t}\n\t\tif powerOnHours > 0 && powerCycles > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/hub/systems/system_systemd_test.go",
    "content": "//go:build testing\n\npackage systems\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetSystemdServiceId(t *testing.T) {\n\tt.Run(\"deterministic output\", func(t *testing.T) {\n\t\tsystemId := \"sys-123\"\n\t\tserviceName := \"nginx.service\"\n\n\t\t// Call multiple times and ensure same result\n\t\tid1 := makeStableHashId(systemId, serviceName)\n\t\tid2 := makeStableHashId(systemId, serviceName)\n\t\tid3 := makeStableHashId(systemId, serviceName)\n\n\t\tassert.Equal(t, id1, id2)\n\t\tassert.Equal(t, id2, id3)\n\t\tassert.NotEmpty(t, id1)\n\t})\n\n\tt.Run(\"different inputs produce different ids\", func(t *testing.T) {\n\t\tsystemId1 := \"sys-123\"\n\t\tsystemId2 := \"sys-456\"\n\t\tserviceName1 := \"nginx.service\"\n\t\tserviceName2 := \"apache.service\"\n\n\t\tid1 := makeStableHashId(systemId1, serviceName1)\n\t\tid2 := makeStableHashId(systemId2, serviceName1)\n\t\tid3 := makeStableHashId(systemId1, serviceName2)\n\t\tid4 := makeStableHashId(systemId2, serviceName2)\n\n\t\t// All IDs should be different\n\t\tassert.NotEqual(t, id1, id2)\n\t\tassert.NotEqual(t, id1, id3)\n\t\tassert.NotEqual(t, id1, id4)\n\t\tassert.NotEqual(t, id2, id3)\n\t\tassert.NotEqual(t, id2, id4)\n\t\tassert.NotEqual(t, id3, id4)\n\t})\n\n\tt.Run(\"consistent length\", func(t *testing.T) {\n\t\ttestCases := []struct {\n\t\t\tsystemId    string\n\t\t\tserviceName string\n\t\t}{\n\t\t\t{\"short\", \"short.service\"},\n\t\t\t{\"very-long-system-id-that-might-be-used-in-practice\", \"very-long-service-name.service\"},\n\t\t\t{\"\", \"empty-system.service\"},\n\t\t\t{\"empty-service\", \"\"},\n\t\t\t{\"\", \"\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tid := makeStableHashId(tc.systemId, tc.serviceName)\n\t\t\t// FNV-32 produces 8 hex characters\n\t\t\tassert.Len(t, id, 8, \"ID should be 8 characters for systemId='%s', serviceName='%s'\", tc.systemId, tc.serviceName)\n\t\t}\n\t})\n\n\tt.Run(\"hexadecimal output\", func(t *testing.T) {\n\t\tid := makeStableHashId(\"test-system\", \"test-service\")\n\t\tassert.NotEmpty(t, id)\n\n\t\t// Should only contain hexadecimal characters\n\t\tfor _, char := range id {\n\t\t\tassert.True(t, (char >= '0' && char <= '9') || (char >= 'a' && char <= 'f'),\n\t\t\t\t\"ID should only contain hexadecimal characters, got: %s\", id)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/hub/systems/system_test.go",
    "content": "//go:build testing\n\npackage systems\n\nimport (\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n)\n\nfunc TestCombinedData_MigrateDeprecatedFields(t *testing.T) {\n\tt.Run(\"Migrate NetworkSent and NetworkRecv to Bandwidth\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tStats: system.Stats{\n\t\t\t\tNetworkSent: 1.5, // 1.5 MB\n\t\t\t\tNetworkRecv: 2.5, // 2.5 MB\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, true)\n\n\t\texpectedSent := uint64(1.5 * 1024 * 1024)\n\t\texpectedRecv := uint64(2.5 * 1024 * 1024)\n\n\t\tif cd.Stats.Bandwidth[0] != expectedSent {\n\t\t\tt.Errorf(\"expected Bandwidth[0] %d, got %d\", expectedSent, cd.Stats.Bandwidth[0])\n\t\t}\n\t\tif cd.Stats.Bandwidth[1] != expectedRecv {\n\t\t\tt.Errorf(\"expected Bandwidth[1] %d, got %d\", expectedRecv, cd.Stats.Bandwidth[1])\n\t\t}\n\t\tif cd.Stats.NetworkSent != 0 || cd.Stats.NetworkRecv != 0 {\n\t\t\tt.Errorf(\"expected NetworkSent and NetworkRecv to be reset, got %f, %f\", cd.Stats.NetworkSent, cd.Stats.NetworkRecv)\n\t\t}\n\t})\n\n\tt.Run(\"Migrate Info.Bandwidth to Info.BandwidthBytes\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tInfo: system.Info{\n\t\t\t\tBandwidth: 10.0, // 10 MB\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, true)\n\n\t\texpected := uint64(10 * 1024 * 1024)\n\t\tif cd.Info.BandwidthBytes != expected {\n\t\t\tt.Errorf(\"expected BandwidthBytes %d, got %d\", expected, cd.Info.BandwidthBytes)\n\t\t}\n\t\tif cd.Info.Bandwidth != 0 {\n\t\t\tt.Errorf(\"expected Info.Bandwidth to be reset, got %f\", cd.Info.Bandwidth)\n\t\t}\n\t})\n\n\tt.Run(\"Migrate DiskReadPs and DiskWritePs to DiskIO\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tStats: system.Stats{\n\t\t\t\tDiskReadPs:  3.0, // 3 MB\n\t\t\t\tDiskWritePs: 4.0, // 4 MB\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, true)\n\n\t\texpectedRead := uint64(3 * 1024 * 1024)\n\t\texpectedWrite := uint64(4 * 1024 * 1024)\n\n\t\tif cd.Stats.DiskIO[0] != expectedRead {\n\t\t\tt.Errorf(\"expected DiskIO[0] %d, got %d\", expectedRead, cd.Stats.DiskIO[0])\n\t\t}\n\t\tif cd.Stats.DiskIO[1] != expectedWrite {\n\t\t\tt.Errorf(\"expected DiskIO[1] %d, got %d\", expectedWrite, cd.Stats.DiskIO[1])\n\t\t}\n\t\tif cd.Stats.DiskReadPs != 0 || cd.Stats.DiskWritePs != 0 {\n\t\t\tt.Errorf(\"expected DiskReadPs and DiskWritePs to be reset, got %f, %f\", cd.Stats.DiskReadPs, cd.Stats.DiskWritePs)\n\t\t}\n\t})\n\n\tt.Run(\"Migrate Info fields to Details struct\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tStats: system.Stats{\n\t\t\t\tMem: 16.0, // 16 GB\n\t\t\t},\n\t\t\tInfo: system.Info{\n\t\t\t\tHostname:      \"test-host\",\n\t\t\t\tKernelVersion: \"6.8.0\",\n\t\t\t\tCores:         8,\n\t\t\t\tThreads:       16,\n\t\t\t\tCpuModel:      \"Intel i7\",\n\t\t\t\tPodman:        true,\n\t\t\t\tOs:            system.Linux,\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, true)\n\n\t\tif cd.Details == nil {\n\t\t\tt.Fatal(\"expected Details struct to be created\")\n\t\t}\n\t\tif cd.Details.Hostname != \"test-host\" {\n\t\t\tt.Errorf(\"expected Hostname 'test-host', got '%s'\", cd.Details.Hostname)\n\t\t}\n\t\tif cd.Details.Kernel != \"6.8.0\" {\n\t\t\tt.Errorf(\"expected Kernel '6.8.0', got '%s'\", cd.Details.Kernel)\n\t\t}\n\t\tif cd.Details.Cores != 8 {\n\t\t\tt.Errorf(\"expected Cores 8, got %d\", cd.Details.Cores)\n\t\t}\n\t\tif cd.Details.Threads != 16 {\n\t\t\tt.Errorf(\"expected Threads 16, got %d\", cd.Details.Threads)\n\t\t}\n\t\tif cd.Details.CpuModel != \"Intel i7\" {\n\t\t\tt.Errorf(\"expected CpuModel 'Intel i7', got '%s'\", cd.Details.CpuModel)\n\t\t}\n\t\tif cd.Details.Podman != true {\n\t\t\tt.Errorf(\"expected Podman true, got %v\", cd.Details.Podman)\n\t\t}\n\t\tif cd.Details.Os != system.Linux {\n\t\t\tt.Errorf(\"expected Os Linux, got %d\", cd.Details.Os)\n\t\t}\n\t\texpectedMem := uint64(16 * 1024 * 1024 * 1024)\n\t\tif cd.Details.MemoryTotal != expectedMem {\n\t\t\tt.Errorf(\"expected MemoryTotal %d, got %d\", expectedMem, cd.Details.MemoryTotal)\n\t\t}\n\n\t\tif cd.Info.Hostname != \"\" || cd.Info.KernelVersion != \"\" || cd.Info.Cores != 0 || cd.Info.CpuModel != \"\" || cd.Info.Podman != false || cd.Info.Os != 0 {\n\t\t\tt.Errorf(\"expected Info fields to be reset, got %+v\", cd.Info)\n\t\t}\n\t})\n\n\tt.Run(\"Do not migrate if Details already exists\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tDetails: &system.Details{Hostname: \"existing-host\"},\n\t\t\tInfo: system.Info{\n\t\t\t\tHostname: \"deprecated-host\",\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, true)\n\n\t\tif cd.Details.Hostname != \"existing-host\" {\n\t\t\tt.Errorf(\"expected Hostname 'existing-host', got '%s'\", cd.Details.Hostname)\n\t\t}\n\t\tif cd.Info.Hostname != \"deprecated-host\" {\n\t\t\tt.Errorf(\"expected Info.Hostname to remain 'deprecated-host', got '%s'\", cd.Info.Hostname)\n\t\t}\n\t})\n\n\tt.Run(\"Do not create details if migrateDetails is false\", func(t *testing.T) {\n\t\tcd := &system.CombinedData{\n\t\t\tInfo: system.Info{\n\t\t\t\tHostname: \"deprecated-host\",\n\t\t\t},\n\t\t}\n\t\tmigrateDeprecatedFields(cd, false)\n\n\t\tif cd.Details != nil {\n\t\t\tt.Fatal(\"expected Details struct to not be created\")\n\t\t}\n\n\t\tif cd.Info.Hostname != \"\" {\n\t\t\tt.Errorf(\"expected Info.Hostname to be reset, got '%s'\", cd.Info.Hostname)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/hub/systems/systems_production.go",
    "content": "//go:build !testing\n\npackage systems\n\n// Background SMART fetching is enabled in production but disabled for tests (systems_test_helpers.go).\n//\n// The hub integration tests create/replace systems and clean up the test apps quickly.\n// Background SMART fetching can outlive teardown and crash in PocketBase internals (nil DB).\nfunc backgroundSmartFetchEnabled() bool { return true }\n"
  },
  {
    "path": "internal/hub/systems/systems_test.go",
    "content": "//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/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/hub/systems\"\n\t\"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSystemManagerNew(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer hub.Cleanup()\n\tsm := hub.GetSystemManager()\n\n\tuser, err := tests.CreateUser(hub, \"test@test.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tsynctest.Test(t, func(t *testing.T) {\n\t\tsm.Initialize()\n\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"it-was-coney-island\",\n\t\t\t\"host\":  \"the-playground-of-the-world\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, \"pending\", record.GetString(\"status\"), \"System status should be 'pending'\")\n\t\tassert.Equal(t, \"pending\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'pending'\")\n\n\t\t// Verify the system host and port\n\t\thost, port := sm.GetSystemHostPort(record.Id)\n\t\tassert.Equal(t, record.GetString(\"host\"), host, \"System host should match\")\n\t\tassert.Equal(t, record.GetString(\"port\"), port, \"System port should match\")\n\n\t\ttime.Sleep(13 * time.Second)\n\t\tsynctest.Wait()\n\n\t\tassert.Equal(t, \"pending\", record.Fresh().GetString(\"status\"), \"System status should be 'pending'\")\n\t\t// Verify the system was added by checking if it exists\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should exist in the store\")\n\n\t\ttime.Sleep(10 * time.Second)\n\t\tsynctest.Wait()\n\n\t\t// system should be set to down after 15 seconds (no websocket connection)\n\t\tassert.Equal(t, \"down\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'down'\")\n\t\t// make sure the system is down in the db\n\t\trecord, err = hub.FindRecordById(\"systems\", record.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"down\", record.GetString(\"status\"), \"System status should be 'down'\")\n\n\t\tassert.Equal(t, 1, sm.GetSystemCount(), \"System count should be 1\")\n\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, 0, sm.GetSystemCount(), \"System count should be 0\")\n\t\tassert.False(t, sm.HasSystem(record.Id), \"System should not exist in the store after removal\")\n\n\t\t// let's also make sure a system is removed from the store when the record is deleted\n\t\trecord, err = tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"there-was-no-place-like-it\",\n\t\t\t\"host\":  \"in-the-whole-world\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should exist in the store after creation\")\n\n\t\ttime.Sleep(8 * time.Second)\n\t\tsynctest.Wait()\n\t\tassert.Equal(t, \"pending\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'pending'\")\n\n\t\tsm.SetSystemStatusInDB(record.Id, \"up\")\n\t\ttime.Sleep(time.Second)\n\t\tsynctest.Wait()\n\t\tassert.Equal(t, \"up\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'up'\")\n\n\t\t// make sure the system switches to down after 11 seconds\n\t\tsm.RemoveSystem(record.Id)\n\t\tsm.AddRecord(record, nil)\n\t\tassert.Equal(t, \"pending\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'pending'\")\n\t\ttime.Sleep(12 * time.Second)\n\t\tsynctest.Wait()\n\t\tassert.Equal(t, \"down\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'down'\")\n\n\t\t// sm.SetSystemStatusInDB(record.Id, \"paused\")\n\t\t// time.Sleep(time.Second)\n\t\t// synctest.Wait()\n\t\t// assert.Equal(t, \"paused\", sm.GetSystemStatusFromStore(record.Id), \"System status should be 'paused'\")\n\n\t\t// delete the record\n\t\terr = hub.Delete(record)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, sm.HasSystem(record.Id), \"System should not exist in the store after deletion\")\n\t})\n\n\ttestOld(t, hub)\n\n\tsynctest.Test(t, func(t *testing.T) {\n\t\ttime.Sleep(time.Second)\n\t\tsynctest.Wait()\n\n\t\tfor _, systemId := range sm.GetAllSystemIDs() {\n\t\t\terr = sm.RemoveSystem(systemId)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.False(t, sm.HasSystem(systemId), \"System should not exist in the store after deletion\")\n\t\t}\n\n\t\tassert.Equal(t, 0, sm.GetSystemCount(), \"System count should be 0\")\n\n\t\t// TODO: test with websocket client\n\t})\n}\n\nfunc testOld(t *testing.T, hub *tests.TestHub) {\n\tuser, err := tests.CreateUser(hub, \"test@testy.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tsm := hub.GetSystemManager()\n\tassert.NotNil(t, sm)\n\n\t// error expected when creating a user with a duplicate email\n\t_, err = tests.CreateUser(hub, \"test@test.com\", \"testtesttest\")\n\trequire.Error(t, err)\n\n\t// Test collection existence. todo: move to hub package tests\n\tt.Run(\"CollectionExistence\", func(t *testing.T) {\n\t\t// Verify that required collections exist\n\t\tsystems, err := hub.FindCachedCollectionByNameOrId(\"systems\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, systems)\n\n\t\tsystemStats, err := hub.FindCachedCollectionByNameOrId(\"system_stats\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, systemStats)\n\n\t\tcontainerStats, err := hub.FindCachedCollectionByNameOrId(\"container_stats\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, containerStats)\n\t})\n\n\tt.Run(\"RemoveSystem\", func(t *testing.T) {\n\t\t// Get the count before adding the system\n\t\tcountBefore := sm.GetSystemCount()\n\n\t\t// Create a test system record\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"i-even-got-lost-at-coney-island\",\n\t\t\t\"host\":  \"but-they-found-me\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Verify the system count increased\n\t\tcountAfterAdd := sm.GetSystemCount()\n\t\tassert.Equal(t, countBefore+1, countAfterAdd, \"System count should increase after adding a system via event hook\")\n\n\t\t// Verify the system exists\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should exist in the store\")\n\n\t\t// Remove the system\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.NoError(t, err)\n\n\t\t// Check that the system count decreased\n\t\tcountAfterRemove := sm.GetSystemCount()\n\t\tassert.Equal(t, countAfterAdd-1, countAfterRemove, \"System count should decrease after removing a system\")\n\n\t\t// Verify the system no longer exists\n\t\tassert.False(t, sm.HasSystem(record.Id), \"System should not exist in the store after removal\")\n\n\t\t// Verify the system is not in the list of all system IDs\n\t\tids := sm.GetAllSystemIDs()\n\t\tassert.NotContains(t, ids, record.Id, \"System ID should not be in the list of all system IDs after removal\")\n\n\t\t// Verify the system status is empty\n\t\tstatus := sm.GetSystemStatusFromStore(record.Id)\n\t\tassert.Equal(t, \"\", status, \"System status should be empty after removal\")\n\n\t\t// Try to remove it again - should return an error since it's already removed\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"NewRecordPending\", func(t *testing.T) {\n\t\t// Create a test system\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"and-you-know\",\n\t\t\t\"host\":  \"i-feel-very-bad\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Add the record to the system manager\n\t\terr = sm.AddRecord(record, nil)\n\t\trequire.NoError(t, err)\n\n\t\t// Test filtering records by status - should be \"pending\" now\n\t\tfilter := \"status = 'pending'\"\n\t\tpendingSystems, err := hub.FindRecordsByFilter(\"systems\", filter, \"-created\", 0, 0, nil)\n\t\trequire.NoError(t, err)\n\t\tassert.GreaterOrEqual(t, len(pendingSystems), 1)\n\t})\n\n\tt.Run(\"SystemStatusUpdate\", func(t *testing.T) {\n\t\t// Create a test system record\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"we-used-to-sleep-on-the-beach\",\n\t\t\t\"host\":  \"sleep-overnight-here\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Add the record to the system manager\n\t\terr = sm.AddRecord(record, nil)\n\t\trequire.NoError(t, err)\n\n\t\t// Test status changes\n\t\tinitialStatus := sm.GetSystemStatusFromStore(record.Id)\n\n\t\t// Set a new status\n\t\tsm.SetSystemStatusInDB(record.Id, \"up\")\n\n\t\t// Verify status was updated\n\t\tnewStatus := sm.GetSystemStatusFromStore(record.Id)\n\t\tassert.Equal(t, \"up\", newStatus, \"System status should be updated to 'up'\")\n\t\tassert.NotEqual(t, initialStatus, newStatus, \"Status should have changed\")\n\n\t\t// Verify the database was updated\n\t\tupdatedRecord, err := hub.FindRecordById(\"systems\", record.Id)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"up\", updatedRecord.Get(\"status\"), \"Database status should match\")\n\t})\n\n\tt.Run(\"HandleSystemData\", func(t *testing.T) {\n\t\t// Create a test system record\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"things-changed-you-know\",\n\t\t\t\"host\":  \"they-dont-sleep-anymore-on-the-beach\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Create test system data\n\t\ttestData := &system.CombinedData{\n\t\t\tDetails: &system.Details{\n\t\t\t\tHostname: \"data-test.example.com\",\n\t\t\t\tKernel:   \"5.15.0-generic\",\n\t\t\t\tCores:    4,\n\t\t\t\tThreads:  8,\n\t\t\t\tCpuModel: \"Test CPU\",\n\t\t\t},\n\t\t\tInfo: system.Info{\n\t\t\t\tUptime:       3600,\n\t\t\t\tCpu:          25.5,\n\t\t\t\tMemPct:       40.2,\n\t\t\t\tDiskPct:      60.0,\n\t\t\t\tBandwidth:    100.0,\n\t\t\t\tAgentVersion: \"1.0.0\",\n\t\t\t},\n\t\t\tStats: system.Stats{\n\t\t\t\tCpu:         25.5,\n\t\t\t\tMem:         16384.0,\n\t\t\t\tMemUsed:     6553.6,\n\t\t\t\tMemPct:      40.0,\n\t\t\t\tDiskTotal:   1024000.0,\n\t\t\t\tDiskUsed:    614400.0,\n\t\t\t\tDiskPct:     60.0,\n\t\t\t\tNetworkSent: 1024.0,\n\t\t\t\tNetworkRecv: 2048.0,\n\t\t\t},\n\t\t\tContainers: []*container.Stats{},\n\t\t}\n\n\t\t// Test handling system data. todo: move to hub/alerts package tests\n\t\terr = hub.HandleSystemAlerts(record, testData)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"ErrorHandling\", func(t *testing.T) {\n\t\t// Try to add a non-existent record\n\t\tnonExistentId := \"non_existent_id\"\n\t\terr := sm.RemoveSystem(nonExistentId)\n\t\tassert.Error(t, err)\n\n\t\t// Try to add a system with invalid host\n\t\tsystem := &systems.System{\n\t\t\tHost: \"\",\n\t\t}\n\t\terr = sm.AddSystem(system)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"ConcurrentOperations\", func(t *testing.T) {\n\t\t// Create a test system\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"jfkjahkfajs\",\n\t\t\t\"host\":  \"localhost\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Run concurrent operations\n\t\tconst goroutines = 5\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(goroutines)\n\n\t\tfor i := range goroutines {\n\t\t\tgo func(i int) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\t// Alternate between different operations\n\t\t\t\tswitch i % 3 {\n\t\t\t\tcase 0:\n\t\t\t\t\tstatus := fmt.Sprintf(\"status-%d\", i)\n\t\t\t\t\tsm.SetSystemStatusInDB(record.Id, status)\n\t\t\t\tcase 1:\n\t\t\t\t\t_ = sm.GetSystemStatusFromStore(record.Id)\n\t\t\t\tcase 2:\n\t\t\t\t\t_, _ = sm.GetSystemHostPort(record.Id)\n\t\t\t\t}\n\t\t\t}(i)\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Verify system still exists and is in a valid state\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should still exist after concurrent operations\")\n\t\tstatus := sm.GetSystemStatusFromStore(record.Id)\n\t\tassert.NotEmpty(t, status, \"System should have a status after concurrent operations\")\n\t})\n\n\tt.Run(\"ContextCancellation\", func(t *testing.T) {\n\t\t// Create a test system record\n\t\trecord, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":  \"lkhsdfsjf\",\n\t\t\t\"host\":  \"localhost\",\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{user.Id},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Verify the system exists in the store\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should exist in the store\")\n\n\t\t// Store the original context and cancel function\n\t\toriginalCtx, originalCancel, err := sm.GetSystemContextFromStore(record.Id)\n\t\tassert.NoError(t, err)\n\n\t\t// Ensure the context is not nil\n\t\tassert.NotNil(t, originalCtx, \"System context should not be nil\")\n\t\tassert.NotNil(t, originalCancel, \"System cancel function should not be nil\")\n\n\t\t// Cancel the context\n\t\toriginalCancel()\n\n\t\t// Wait a short time for cancellation to propagate\n\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t// Verify the context is done\n\t\tselect {\n\t\tcase <-originalCtx.Done():\n\t\t\t// Context was properly cancelled\n\t\tdefault:\n\t\t\tt.Fatal(\"Context was not cancelled\")\n\t\t}\n\n\t\t// Verify the system is still in the store (cancellation shouldn't remove it)\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should still exist after context cancellation\")\n\n\t\t// Explicitly remove the system\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.NoError(t, err, \"RemoveSystem should succeed\")\n\n\t\t// Verify the system is removed\n\t\tassert.False(t, sm.HasSystem(record.Id), \"System should be removed after RemoveSystem\")\n\n\t\t// Try to remove it again - should return an error\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.Error(t, err, \"RemoveSystem should fail for non-existent system\")\n\n\t\t// Add the system back\n\t\terr = sm.AddRecord(record, nil)\n\t\trequire.NoError(t, err, \"AddRecord should succeed\")\n\n\t\t// Verify the system is back in the store\n\t\tassert.True(t, sm.HasSystem(record.Id), \"System should exist after re-adding\")\n\n\t\t// Verify a new context was created\n\t\tnewCtx, newCancel, err := sm.GetSystemContextFromStore(record.Id)\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, newCtx, \"New system context should not be nil\")\n\t\tassert.NotNil(t, newCancel, \"New system cancel function should not be nil\")\n\t\tassert.NotEqual(t, originalCtx, newCtx, \"New context should be different from original\")\n\n\t\t// Clean up\n\t\terr = sm.RemoveSystem(record.Id)\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/hub/systems/systems_test_helpers.go",
    "content": "//go:build testing\n\npackage systems\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tentities \"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// The hub integration tests create/replace systems and cleanup the test apps quickly.\n// Background SMART fetching can outlive teardown and crash in PocketBase internals (nil DB).\n//\n// We keep the explicit SMART refresh endpoint / method available, but disable\n// the automatic background fetch during tests.\nfunc backgroundSmartFetchEnabled() bool { return false }\n\n// TESTING ONLY: GetSystemCount returns the number of systems in the store\nfunc (sm *SystemManager) GetSystemCount() int {\n\treturn sm.systems.Length()\n}\n\n// TESTING ONLY: HasSystem checks if a system with the given ID exists in the store\nfunc (sm *SystemManager) HasSystem(systemID string) bool {\n\treturn sm.systems.Has(systemID)\n}\n\n// TESTING ONLY: GetSystemStatusFromStore returns the status of a system with the given ID\n// Returns an empty string if the system doesn't exist\nfunc (sm *SystemManager) GetSystemStatusFromStore(systemID string) string {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\treturn sys.Status\n}\n\n// TESTING ONLY: GetSystemContextFromStore returns the context and cancel function for a system\nfunc (sm *SystemManager) GetSystemContextFromStore(systemID string) (context.Context, context.CancelFunc, error) {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"no system\")\n\t}\n\treturn sys.ctx, sys.cancel, nil\n}\n\n// TESTING ONLY: GetSystemFromStore returns a store from the system\nfunc (sm *SystemManager) GetSystemFromStore(systemID string) (*System, error) {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no system\")\n\t}\n\treturn sys, nil\n}\n\n// TESTING ONLY: GetAllSystemIDs returns a slice of all system IDs in the store\nfunc (sm *SystemManager) GetAllSystemIDs() []string {\n\tdata := sm.systems.GetAll()\n\tids := make([]string, 0, len(data))\n\tfor id := range data {\n\t\tids = append(ids, id)\n\t}\n\treturn ids\n}\n\n// TESTING ONLY: GetSystemData returns the combined data for a system with the given ID\n// Returns nil if the system doesn't exist\n// This method is intended for testing\nfunc (sm *SystemManager) GetSystemData(systemID string) *entities.CombinedData {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn sys.data\n}\n\n// TESTING ONLY: GetSystemHostPort returns the host and port for a system with the given ID\n// Returns empty strings if the system doesn't exist\nfunc (sm *SystemManager) GetSystemHostPort(systemID string) (string, string) {\n\tsys, ok := sm.systems.GetOk(systemID)\n\tif !ok {\n\t\treturn \"\", \"\"\n\t}\n\treturn sys.Host, sys.Port\n}\n\n// TESTING ONLY: SetSystemStatusInDB sets the status of a system directly and updates the database record\n// This is intended for testing\n// Returns false if the system doesn't exist\nfunc (sm *SystemManager) SetSystemStatusInDB(systemID string, status string) bool {\n\tif !sm.HasSystem(systemID) {\n\t\treturn false\n\t}\n\n\t// Update the database record\n\trecord, err := sm.hub.FindRecordById(\"systems\", systemID)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\trecord.Set(\"status\", status)\n\terr = sm.hub.Save(record)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// TESTING ONLY: RemoveAllSystems removes all systems from the store\nfunc (sm *SystemManager) RemoveAllSystems() {\n\tfor _, system := range sm.systems.GetAll() {\n\t\tsm.RemoveSystem(system.Id)\n\t}\n\tsm.smartFetchMap.StopCleaner()\n}\n\nfunc (s *System) StopUpdater() {\n\ts.cancel()\n}\n\nfunc (s *System) CreateRecords(data *entities.CombinedData) (*core.Record, error) {\n\ts.data = data\n\treturn s.createRecords(data)\n}\n"
  },
  {
    "path": "internal/hub/transport/ssh.go",
    "content": "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\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/henrygd/beszel/internal/common\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// SSHTransport implements Transport over SSH connections.\ntype SSHTransport struct {\n\tclient       *ssh.Client\n\tconfig       *ssh.ClientConfig\n\thost         string\n\tport         string\n\tagentVersion semver.Version\n\ttimeout      time.Duration\n}\n\n// SSHTransportConfig holds configuration for creating an SSH transport.\ntype SSHTransportConfig struct {\n\tHost         string\n\tPort         string\n\tConfig       *ssh.ClientConfig\n\tAgentVersion semver.Version\n\tTimeout      time.Duration\n}\n\n// NewSSHTransport creates a new SSH transport with the given configuration.\nfunc NewSSHTransport(cfg SSHTransportConfig) *SSHTransport {\n\ttimeout := cfg.Timeout\n\tif timeout == 0 {\n\t\ttimeout = 4 * time.Second\n\t}\n\treturn &SSHTransport{\n\t\tconfig:       cfg.Config,\n\t\thost:         cfg.Host,\n\t\tport:         cfg.Port,\n\t\tagentVersion: cfg.AgentVersion,\n\t\ttimeout:      timeout,\n\t}\n}\n\n// SetClient sets the SSH client for reuse across requests.\nfunc (t *SSHTransport) SetClient(client *ssh.Client) {\n\tt.client = client\n}\n\n// SetAgentVersion sets the agent version (extracted from SSH handshake).\nfunc (t *SSHTransport) SetAgentVersion(version semver.Version) {\n\tt.agentVersion = version\n}\n\n// GetClient returns the current SSH client (for connection management).\nfunc (t *SSHTransport) GetClient() *ssh.Client {\n\treturn t.client\n}\n\n// GetAgentVersion returns the agent version.\nfunc (t *SSHTransport) GetAgentVersion() semver.Version {\n\treturn t.agentVersion\n}\n\n// Request sends a request to the agent via SSH and unmarshals the response.\nfunc (t *SSHTransport) Request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {\n\tif t.client == nil {\n\t\tif err := t.connect(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tsession, err := t.createSessionWithTimeout(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer session.Close()\n\n\tstdout, err := session.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tstdin, err := session.StdinPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := session.Shell(); err != nil {\n\t\treturn err\n\t}\n\n\t// Send request\n\thubReq := common.HubRequest[any]{Action: action, Data: req}\n\tif err := cbor.NewEncoder(stdin).Encode(hubReq); err != nil {\n\t\treturn fmt.Errorf(\"failed to encode request: %w\", err)\n\t}\n\tstdin.Close()\n\n\t// Read response\n\tvar resp common.AgentResponse\n\tif err := cbor.NewDecoder(stdout).Decode(&resp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\n\tif resp.Error != \"\" {\n\t\treturn errors.New(resp.Error)\n\t}\n\n\tif err := session.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\treturn UnmarshalResponse(resp, action, dest)\n}\n\n// IsConnected returns true if the SSH connection is active.\nfunc (t *SSHTransport) IsConnected() bool {\n\treturn t.client != nil\n}\n\n// Close terminates the SSH connection.\nfunc (t *SSHTransport) Close() {\n\tif t.client != nil {\n\t\tt.client.Close()\n\t\tt.client = nil\n\t}\n}\n\n// connect establishes a new SSH connection.\nfunc (t *SSHTransport) connect() error {\n\tif t.config == nil {\n\t\treturn errors.New(\"SSH config not set\")\n\t}\n\n\tnetwork := \"tcp\"\n\thost := t.host\n\tif strings.HasPrefix(host, \"/\") {\n\t\tnetwork = \"unix\"\n\t} else {\n\t\thost = net.JoinHostPort(host, t.port)\n\t}\n\n\tclient, err := ssh.Dial(network, host, t.config)\n\tif err != nil {\n\t\treturn err\n\t}\n\tt.client = client\n\n\t// Extract agent version from server version string\n\tt.agentVersion, _ = extractAgentVersion(string(client.Conn.ServerVersion()))\n\treturn nil\n}\n\n// createSessionWithTimeout creates a new SSH session with a timeout.\nfunc (t *SSHTransport) createSessionWithTimeout(ctx context.Context) (*ssh.Session, error) {\n\tif t.client == nil {\n\t\treturn nil, errors.New(\"client not initialized\")\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, t.timeout)\n\tdefer cancel()\n\n\tsessionChan := make(chan *ssh.Session, 1)\n\terrChan := make(chan error, 1)\n\n\tgo func() {\n\t\tsession, err := t.client.NewSession()\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t} else {\n\t\t\tsessionChan <- session\n\t\t}\n\t}()\n\n\tselect {\n\tcase session := <-sessionChan:\n\t\treturn session, nil\n\tcase err := <-errChan:\n\t\treturn nil, err\n\tcase <-ctx.Done():\n\t\treturn nil, errors.New(\"timeout creating session\")\n\t}\n}\n\n// extractAgentVersion extracts the beszel version from SSH server version string.\nfunc extractAgentVersion(versionString string) (semver.Version, error) {\n\t_, after, _ := strings.Cut(versionString, \"_\")\n\treturn semver.Parse(after)\n}\n\n// RequestWithRetry sends a request with automatic retry on connection failures.\nfunc (t *SSHTransport) RequestWithRetry(ctx context.Context, action common.WebSocketAction, req any, dest any, retries int) error {\n\tvar lastErr error\n\tfor attempt := 0; attempt <= retries; attempt++ {\n\t\terr := t.Request(ctx, action, req, dest)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tlastErr = err\n\n\t\t// Check if it's a connection error that warrants a retry\n\t\tif isConnectionError(err) && attempt < retries {\n\t\t\tt.Close()\n\t\t\tcontinue\n\t\t}\n\t\treturn err\n\t}\n\treturn lastErr\n}\n\n// isConnectionError checks if an error indicates a connection problem.\nfunc isConnectionError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\terrStr := err.Error()\n\treturn strings.Contains(errStr, \"connection\") ||\n\t\tstrings.Contains(errStr, \"EOF\") ||\n\t\tstrings.Contains(errStr, \"closed\") ||\n\t\terrors.Is(err, io.EOF)\n}\n"
  },
  {
    "path": "internal/hub/transport/transport.go",
    "content": "// Package transport provides a unified abstraction for hub-agent communication\n// over different transports (WebSocket, SSH).\npackage transport\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/internal/common\"\n\t\"github.com/henrygd/beszel/internal/entities/smart\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\t\"github.com/henrygd/beszel/internal/entities/systemd\"\n)\n\n// Transport defines the interface for hub-agent communication.\n// Both WebSocket and SSH transports implement this interface.\ntype Transport interface {\n\t// Request sends a request to the agent and unmarshals the response into dest.\n\t// The dest parameter should be a pointer to the expected response type.\n\tRequest(ctx context.Context, action common.WebSocketAction, req any, dest any) error\n\t// IsConnected returns true if the transport connection is active.\n\tIsConnected() bool\n\t// Close terminates the transport connection.\n\tClose()\n}\n\n// UnmarshalResponse unmarshals an AgentResponse into the destination type.\n// It first checks the generic Data field (0.19+ agents), then falls back\n// to legacy typed fields for backward compatibility with 0.18.0 agents.\nfunc UnmarshalResponse(resp common.AgentResponse, action common.WebSocketAction, dest any) error {\n\tif dest == nil {\n\t\treturn errors.New(\"nil destination\")\n\t}\n\t// Try generic Data field first (0.19+)\n\tif len(resp.Data) > 0 {\n\t\tif err := cbor.Unmarshal(resp.Data, dest); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal generic response data: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\t// Fall back to legacy typed fields for older agents/hubs.\n\treturn unmarshalLegacyResponse(resp, action, dest)\n}\n\n// unmarshalLegacyResponse handles legacy responses that use typed fields.\nfunc unmarshalLegacyResponse(resp common.AgentResponse, action common.WebSocketAction, dest any) error {\n\tswitch action {\n\tcase common.GetData:\n\t\td, ok := dest.(*system.CombinedData)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for GetData: %T\", dest)\n\t\t}\n\t\tif resp.SystemData == nil {\n\t\t\treturn errors.New(\"no system data in response\")\n\t\t}\n\t\t*d = *resp.SystemData\n\t\treturn nil\n\tcase common.CheckFingerprint:\n\t\td, ok := dest.(*common.FingerprintResponse)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for CheckFingerprint: %T\", dest)\n\t\t}\n\t\tif resp.Fingerprint == nil {\n\t\t\treturn errors.New(\"no fingerprint in response\")\n\t\t}\n\t\t*d = *resp.Fingerprint\n\t\treturn nil\n\tcase common.GetContainerLogs:\n\t\td, ok := dest.(*string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for GetContainerLogs: %T\", dest)\n\t\t}\n\t\tif resp.String == nil {\n\t\t\treturn errors.New(\"no logs in response\")\n\t\t}\n\t\t*d = *resp.String\n\t\treturn nil\n\tcase common.GetContainerInfo:\n\t\td, ok := dest.(*string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for GetContainerInfo: %T\", dest)\n\t\t}\n\t\tif resp.String == nil {\n\t\t\treturn errors.New(\"no info in response\")\n\t\t}\n\t\t*d = *resp.String\n\t\treturn nil\n\tcase common.GetSmartData:\n\t\td, ok := dest.(*map[string]smart.SmartData)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for GetSmartData: %T\", dest)\n\t\t}\n\t\tif resp.SmartData == nil {\n\t\t\treturn errors.New(\"no SMART data in response\")\n\t\t}\n\t\t*d = resp.SmartData\n\t\treturn nil\n\tcase common.GetSystemdInfo:\n\t\td, ok := dest.(*systemd.ServiceDetails)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unexpected dest type for GetSystemdInfo: %T\", dest)\n\t\t}\n\t\tif resp.ServiceInfo == nil {\n\t\t\treturn errors.New(\"no systemd info in response\")\n\t\t}\n\t\t*d = resp.ServiceInfo\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"unsupported action: %d\", action)\n}\n"
  },
  {
    "path": "internal/hub/transport/websocket.go",
    "content": "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.com/henrygd/beszel/internal/common\"\n\t\"github.com/henrygd/beszel/internal/hub/ws\"\n)\n\n// ErrWebSocketNotConnected indicates a WebSocket transport is not currently connected.\nvar ErrWebSocketNotConnected = errors.New(\"websocket not connected\")\n\n// WebSocketTransport implements Transport over WebSocket connections.\ntype WebSocketTransport struct {\n\twsConn *ws.WsConn\n}\n\n// NewWebSocketTransport creates a new WebSocket transport wrapper.\nfunc NewWebSocketTransport(wsConn *ws.WsConn) *WebSocketTransport {\n\treturn &WebSocketTransport{wsConn: wsConn}\n}\n\n// Request sends a request to the agent via WebSocket and unmarshals the response.\nfunc (t *WebSocketTransport) Request(ctx context.Context, action common.WebSocketAction, req any, dest any) error {\n\tif !t.IsConnected() {\n\t\treturn ErrWebSocketNotConnected\n\t}\n\n\tpendingReq, err := t.wsConn.SendRequest(ctx, action, req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Wait for response\n\tselect {\n\tcase message := <-pendingReq.ResponseCh:\n\t\tdefer message.Close()\n\t\tdefer pendingReq.Cancel()\n\n\t\t// Legacy agents (< MinVersionAgentResponse) respond with a raw payload instead of an AgentResponse wrapper.\n\t\tif t.wsConn.AgentVersion().LT(beszel.MinVersionAgentResponse) {\n\t\t\treturn cbor.Unmarshal(message.Data.Bytes(), dest)\n\t\t}\n\n\t\tvar agentResponse common.AgentResponse\n\t\tif err := cbor.Unmarshal(message.Data.Bytes(), &agentResponse); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif agentResponse.Error != \"\" {\n\t\t\treturn errors.New(agentResponse.Error)\n\t\t}\n\n\t\treturn UnmarshalResponse(agentResponse, action, dest)\n\n\tcase <-pendingReq.Context.Done():\n\t\treturn pendingReq.Context.Err()\n\t}\n}\n\n// IsConnected returns true if the WebSocket connection is active.\nfunc (t *WebSocketTransport) IsConnected() bool {\n\treturn t.wsConn != nil && t.wsConn.IsConnected()\n}\n\n// Close terminates the WebSocket connection.\nfunc (t *WebSocketTransport) Close() {\n\tif t.wsConn != nil {\n\t\tt.wsConn.Close(nil)\n\t}\n}\n"
  },
  {
    "path": "internal/hub/update.go",
    "content": "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/cobra\"\n)\n\n// Update updates beszel to the latest version\nfunc Update(cmd *cobra.Command, _ []string) {\n\tdataDir := os.TempDir()\n\n\t// set dataDir to ./beszel_data if it exists\n\tif _, err := os.Stat(\"./beszel_data\"); err == nil {\n\t\tdataDir = \"./beszel_data\"\n\t}\n\n\t// Check if china-mirrors flag is set\n\tuseMirror, _ := cmd.Flags().GetBool(\"china-mirrors\")\n\n\t// Get the executable path before update\n\texePath, err := os.Executable()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tupdated, err := ghupdate.Update(ghupdate.Config{\n\t\tArchiveExecutable: \"beszel\",\n\t\tDataDir:           dataDir,\n\t\tUseMirror:         useMirror,\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif !updated {\n\t\treturn\n\t}\n\n\t// make sure the file is executable\n\tif err := os.Chmod(exePath, 0755); err != nil {\n\t\tfmt.Printf(\"Warning: failed to set executable permissions: %v\\n\", err)\n\t}\n\n\t// Fix SELinux context if necessary\n\tif err := ghupdate.HandleSELinuxContext(exePath); err != nil {\n\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: SELinux context handling: %v\", err)\n\t}\n\n\t// Try to restart the service if it's running\n\trestartService()\n}\n\n// restartService attempts to restart the beszel service\nfunc restartService() {\n\t// Check if we're running as a service by looking for systemd\n\tif _, err := exec.LookPath(\"systemctl\"); err == nil {\n\t\t// Check if beszel service exists and is active\n\t\tcmd := exec.Command(\"systemctl\", \"is-active\", \"beszel.service\")\n\t\tif err := cmd.Run(); err == nil {\n\t\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel service...\")\n\t\t\trestartCmd := exec.Command(\"systemctl\", \"restart\", \"beszel.service\")\n\t\t\tif err := restartCmd.Run(); err != nil {\n\t\t\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: Failed to restart service: %v\\n\", err)\n\t\t\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Please restart the service manually: sudo systemctl restart beszel\")\n\t\t\t} else {\n\t\t\t\tghupdate.ColorPrint(ghupdate.ColorGreen, \"Service restarted successfully\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Check for OpenRC (Alpine Linux)\n\tif _, err := exec.LookPath(\"rc-service\"); err == nil {\n\t\tcmd := exec.Command(\"rc-service\", \"beszel\", \"status\")\n\t\tif err := cmd.Run(); err == nil {\n\t\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Restarting beszel service...\")\n\t\t\trestartCmd := exec.Command(\"rc-service\", \"beszel\", \"restart\")\n\t\t\tif err := restartCmd.Run(); err != nil {\n\t\t\t\tghupdate.ColorPrintf(ghupdate.ColorYellow, \"Warning: Failed to restart service: %v\\n\", err)\n\t\t\t\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Please restart the service manually: sudo rc-service beszel restart\")\n\t\t\t} else {\n\t\t\t\tghupdate.ColorPrint(ghupdate.ColorGreen, \"Service restarted successfully\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\tghupdate.ColorPrint(ghupdate.ColorYellow, \"Service restart not attempted. If running as a service, restart manually.\")\n}\n"
  },
  {
    "path": "internal/hub/ws/handlers.go",
    "content": "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\t\"github.com/lxzan/gws\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// ResponseHandler defines interface for handling agent responses.\n// This is used by handleAgentRequest for legacy response handling.\ntype ResponseHandler interface {\n\tHandle(agentResponse common.AgentResponse) error\n\tHandleLegacy(rawData []byte) error\n}\n\n// BaseHandler provides a default implementation that can be embedded to make HandleLegacy optional\ntype BaseHandler struct{}\n\nfunc (h *BaseHandler) HandleLegacy(rawData []byte) error {\n\treturn errors.New(\"legacy format not supported\")\n}\n\n////////////////////////////////////////////////////////////////////////////\n// Fingerprint handling (used for WebSocket authentication)\n////////////////////////////////////////////////////////////////////////////\n\n// fingerprintHandler implements ResponseHandler for fingerprint requests\ntype fingerprintHandler struct {\n\tresult *common.FingerprintResponse\n}\n\nfunc (h *fingerprintHandler) HandleLegacy(rawData []byte) error {\n\treturn cbor.Unmarshal(rawData, h.result)\n}\n\nfunc (h *fingerprintHandler) Handle(agentResponse common.AgentResponse) error {\n\tif agentResponse.Fingerprint != nil {\n\t\t*h.result = *agentResponse.Fingerprint\n\t\treturn nil\n\t}\n\treturn errors.New(\"no fingerprint data in response\")\n}\n\n// GetFingerprint authenticates with the agent using SSH signature and returns the agent's fingerprint.\nfunc (ws *WsConn) GetFingerprint(ctx context.Context, token string, signer ssh.Signer, needSysInfo bool) (common.FingerprintResponse, error) {\n\tif !ws.IsConnected() {\n\t\treturn common.FingerprintResponse{}, gws.ErrConnClosed\n\t}\n\n\tchallenge := []byte(token)\n\tsignature, err := signer.Sign(nil, challenge)\n\tif err != nil {\n\t\treturn common.FingerprintResponse{}, err\n\t}\n\n\treq, err := ws.requestManager.SendRequest(ctx, common.CheckFingerprint, common.FingerprintRequest{\n\t\tSignature:   signature.Blob,\n\t\tNeedSysInfo: needSysInfo,\n\t})\n\tif err != nil {\n\t\treturn common.FingerprintResponse{}, err\n\t}\n\n\tvar result common.FingerprintResponse\n\thandler := &fingerprintHandler{result: &result}\n\terr = ws.handleAgentRequest(req, handler)\n\treturn result, err\n}\n"
  },
  {
    "path": "internal/hub/ws/request_manager.go",
    "content": "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/henrygd/beszel/internal/common\"\n\t\"github.com/lxzan/gws\"\n)\n\n// RequestID uniquely identifies a request\ntype RequestID uint32\n\n// PendingRequest tracks an in-flight request\ntype PendingRequest struct {\n\tID         RequestID\n\tResponseCh chan *gws.Message\n\tContext    context.Context\n\tCancel     context.CancelFunc\n\tCreatedAt  time.Time\n}\n\n// RequestManager handles concurrent requests to an agent\ntype RequestManager struct {\n\tsync.RWMutex\n\tconn        *gws.Conn\n\tpendingReqs map[RequestID]*PendingRequest\n\tnextID      atomic.Uint32\n}\n\n// NewRequestManager creates a new request manager for a WebSocket connection\nfunc NewRequestManager(conn *gws.Conn) *RequestManager {\n\trm := &RequestManager{\n\t\tconn:        conn,\n\t\tpendingReqs: make(map[RequestID]*PendingRequest),\n\t}\n\treturn rm\n}\n\n// SendRequest sends a request and returns a channel for the response\nfunc (rm *RequestManager) SendRequest(ctx context.Context, action common.WebSocketAction, data any) (*PendingRequest, error) {\n\treqID := RequestID(rm.nextID.Add(1))\n\n\t// Respect any caller-provided deadline. If none is set, apply a reasonable default\n\t// so pending requests don't live forever if the agent never responds.\n\treqCtx := ctx\n\tvar cancel context.CancelFunc\n\tif _, hasDeadline := ctx.Deadline(); hasDeadline {\n\t\treqCtx, cancel = context.WithCancel(ctx)\n\t} else {\n\t\treqCtx, cancel = context.WithTimeout(ctx, 5*time.Second)\n\t}\n\n\treq := &PendingRequest{\n\t\tID:         reqID,\n\t\tResponseCh: make(chan *gws.Message, 1),\n\t\tContext:    reqCtx,\n\t\tCancel:     cancel,\n\t\tCreatedAt:  time.Now(),\n\t}\n\n\trm.Lock()\n\trm.pendingReqs[reqID] = req\n\trm.Unlock()\n\n\thubReq := common.HubRequest[any]{\n\t\tId:     (*uint32)(&reqID),\n\t\tAction: action,\n\t\tData:   data,\n\t}\n\n\t// Send the request\n\tif err := rm.sendMessage(hubReq); err != nil {\n\t\trm.cancelRequest(reqID)\n\t\treturn nil, fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\n\t// Start cleanup watcher for timeout/cancellation\n\tgo rm.cleanupRequest(req)\n\n\treturn req, nil\n}\n\n// sendMessage encodes and sends a message over WebSocket\nfunc (rm *RequestManager) sendMessage(data any) error {\n\tif rm.conn == nil {\n\t\treturn gws.ErrConnClosed\n\t}\n\n\tbytes, err := cbor.Marshal(data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treturn rm.conn.WriteMessage(gws.OpcodeBinary, bytes)\n}\n\n// handleResponse processes a single response message\nfunc (rm *RequestManager) handleResponse(message *gws.Message) {\n\tvar response common.AgentResponse\n\tif err := cbor.Unmarshal(message.Data.Bytes(), &response); err != nil {\n\t\t// Legacy response without ID - route to first pending request of any type\n\t\trm.routeLegacyResponse(message)\n\t\treturn\n\t}\n\n\tif response.Id == nil {\n\t\trm.routeLegacyResponse(message)\n\t\treturn\n\t}\n\n\treqID := RequestID(*response.Id)\n\n\trm.RLock()\n\treq, exists := rm.pendingReqs[reqID]\n\trm.RUnlock()\n\n\tif !exists {\n\t\t// Request not found (might have timed out) - close the message\n\t\tmessage.Close()\n\t\treturn\n\t}\n\n\tselect {\n\tcase req.ResponseCh <- message:\n\t\t// Message successfully delivered - the receiver will close it\n\t\trm.deleteRequest(reqID)\n\tcase <-req.Context.Done():\n\t\t// Request was cancelled/timed out - close the message\n\t\tmessage.Close()\n\t}\n}\n\n// routeLegacyResponse handles responses that don't have request IDs (backwards compatibility)\nfunc (rm *RequestManager) routeLegacyResponse(message *gws.Message) {\n\t// Snapshot the oldest pending request without holding the lock during send\n\trm.RLock()\n\tvar oldestReq *PendingRequest\n\tfor _, req := range rm.pendingReqs {\n\t\tif oldestReq == nil || req.CreatedAt.Before(oldestReq.CreatedAt) {\n\t\t\toldestReq = req\n\t\t}\n\t}\n\trm.RUnlock()\n\n\tif oldestReq != nil {\n\t\tselect {\n\t\tcase oldestReq.ResponseCh <- message:\n\t\t\t// Message successfully delivered - the receiver will close it\n\t\t\trm.deleteRequest(oldestReq.ID)\n\t\tcase <-oldestReq.Context.Done():\n\t\t\t// Request was cancelled - close the message\n\t\t\tmessage.Close()\n\t\t}\n\t} else {\n\t\t// No pending requests - close the message\n\t\tmessage.Close()\n\t}\n}\n\n// cleanupRequest handles request timeout and cleanup\nfunc (rm *RequestManager) cleanupRequest(req *PendingRequest) {\n\t<-req.Context.Done()\n\trm.cancelRequest(req.ID)\n}\n\n// cancelRequest removes a request and cancels its context\nfunc (rm *RequestManager) cancelRequest(reqID RequestID) {\n\trm.Lock()\n\tdefer rm.Unlock()\n\n\tif req, exists := rm.pendingReqs[reqID]; exists {\n\t\treq.Cancel()\n\t\tdelete(rm.pendingReqs, reqID)\n\t}\n}\n\n// deleteRequest removes a request from the pending map without cancelling its context.\nfunc (rm *RequestManager) deleteRequest(reqID RequestID) {\n\trm.Lock()\n\tdefer rm.Unlock()\n\tdelete(rm.pendingReqs, reqID)\n}\n\n// Close shuts down the request manager\nfunc (rm *RequestManager) Close() {\n\trm.Lock()\n\tdefer rm.Unlock()\n\n\t// Cancel all pending requests\n\tfor _, req := range rm.pendingReqs {\n\t\treq.Cancel()\n\t}\n\trm.pendingReqs = make(map[RequestID]*PendingRequest)\n}\n"
  },
  {
    "path": "internal/hub/ws/request_manager_test.go",
    "content": "//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// TestRequestManager_BasicFunctionality tests the request manager without mocking gws.Conn\nfunc TestRequestManager_BasicFunctionality(t *testing.T) {\n\t// We'll test the core logic without mocking the connection\n\t// since the gws.Conn interface is complex to mock properly\n\n\tt.Run(\"request ID generation\", func(t *testing.T) {\n\t\t// Test that request IDs are generated sequentially and uniquely\n\t\trm := &RequestManager{}\n\n\t\t// Simulate multiple ID generations\n\t\tid1 := rm.nextID.Add(1)\n\t\tid2 := rm.nextID.Add(1)\n\t\tid3 := rm.nextID.Add(1)\n\n\t\tassert.NotEqual(t, id1, id2)\n\t\tassert.NotEqual(t, id2, id3)\n\t\tassert.Greater(t, id2, id1)\n\t\tassert.Greater(t, id3, id2)\n\t})\n\n\tt.Run(\"pending request tracking\", func(t *testing.T) {\n\t\trm := &RequestManager{\n\t\t\tpendingReqs: make(map[RequestID]*PendingRequest),\n\t\t}\n\n\t\t// Initially no pending requests\n\t\tassert.Equal(t, 0, rm.GetPendingCount())\n\n\t\t// Add some fake pending requests\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\n\t\treq1 := &PendingRequest{\n\t\t\tID:      RequestID(1),\n\t\t\tContext: ctx,\n\t\t\tCancel:  cancel,\n\t\t}\n\t\treq2 := &PendingRequest{\n\t\t\tID:      RequestID(2),\n\t\t\tContext: ctx,\n\t\t\tCancel:  cancel,\n\t\t}\n\n\t\trm.pendingReqs[req1.ID] = req1\n\t\trm.pendingReqs[req2.ID] = req2\n\n\t\tassert.Equal(t, 2, rm.GetPendingCount())\n\n\t\t// Remove one\n\t\tdelete(rm.pendingReqs, req1.ID)\n\t\tassert.Equal(t, 1, rm.GetPendingCount())\n\n\t\t// Remove all\n\t\tdelete(rm.pendingReqs, req2.ID)\n\t\tassert.Equal(t, 0, rm.GetPendingCount())\n\t})\n\n\tt.Run(\"context cancellation\", func(t *testing.T) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)\n\t\tdefer cancel()\n\n\t\t// Wait for context to timeout\n\t\t<-ctx.Done()\n\n\t\t// Verify context was cancelled\n\t\tassert.Equal(t, context.DeadlineExceeded, ctx.Err())\n\t})\n}\n"
  },
  {
    "path": "internal/hub/ws/ws.go",
    "content": "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\"github.com/henrygd/beszel/internal/common\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/lxzan/gws\"\n)\n\nconst (\n\tdeadline = 70 * time.Second\n)\n\n// Handler implements the WebSocket event handler for agent connections.\ntype Handler struct {\n\tgws.BuiltinEventHandler\n}\n\n// WsConn represents a WebSocket connection to an agent.\ntype WsConn struct {\n\tconn           *gws.Conn\n\trequestManager *RequestManager\n\tDownChan       chan struct{}\n\tagentVersion   semver.Version\n}\n\n// FingerprintRecord is fingerprints collection record data in the hub\ntype FingerprintRecord struct {\n\tId          string `db:\"id\"`\n\tSystemId    string `db:\"system\"`\n\tFingerprint string `db:\"fingerprint\"`\n\tToken       string `db:\"token\"`\n}\n\nvar upgrader *gws.Upgrader\n\n// GetUpgrader returns a singleton WebSocket upgrader instance.\nfunc GetUpgrader() *gws.Upgrader {\n\tif upgrader != nil {\n\t\treturn upgrader\n\t}\n\thandler := &Handler{}\n\tupgrader = gws.NewUpgrader(handler, &gws.ServerOption{})\n\treturn upgrader\n}\n\n// NewWsConnection creates a new WebSocket connection wrapper with agent version.\nfunc NewWsConnection(conn *gws.Conn, agentVersion semver.Version) *WsConn {\n\treturn &WsConn{\n\t\tconn:           conn,\n\t\trequestManager: NewRequestManager(conn),\n\t\tDownChan:       make(chan struct{}, 1),\n\t\tagentVersion:   agentVersion,\n\t}\n}\n\n// OnOpen sets a deadline for the WebSocket connection and extracts agent version.\nfunc (h *Handler) OnOpen(conn *gws.Conn) {\n\tconn.SetDeadline(time.Now().Add(deadline))\n}\n\n// OnMessage routes incoming WebSocket messages to the request manager.\nfunc (h *Handler) OnMessage(conn *gws.Conn, message *gws.Message) {\n\tconn.SetDeadline(time.Now().Add(deadline))\n\tif message.Opcode != gws.OpcodeBinary || message.Data.Len() == 0 {\n\t\treturn\n\t}\n\twsConn, ok := conn.Session().Load(\"wsConn\")\n\tif !ok {\n\t\t_ = conn.WriteClose(1000, nil)\n\t\treturn\n\t}\n\twsConn.(*WsConn).requestManager.handleResponse(message)\n}\n\n// OnClose handles WebSocket connection closures and triggers system down status after delay.\nfunc (h *Handler) OnClose(conn *gws.Conn, err error) {\n\twsConn, ok := conn.Session().Load(\"wsConn\")\n\tif !ok {\n\t\treturn\n\t}\n\twsConn.(*WsConn).conn = nil\n\t// wait 5 seconds to allow reconnection before setting system down\n\t// use a weak pointer to avoid keeping references if the system is removed\n\tgo func(downChan weak.Pointer[chan struct{}]) {\n\t\ttime.Sleep(5 * time.Second)\n\t\tdownChanValue := downChan.Value()\n\t\tif downChanValue != nil {\n\t\t\t*downChanValue <- struct{}{}\n\t\t}\n\t}(weak.Make(&wsConn.(*WsConn).DownChan))\n}\n\n// Close terminates the WebSocket connection gracefully.\nfunc (ws *WsConn) Close(msg []byte) {\n\tif ws.IsConnected() {\n\t\tws.conn.WriteClose(1000, msg)\n\t}\n\tif ws.requestManager != nil {\n\t\tws.requestManager.Close()\n\t}\n}\n\n// Ping sends a ping frame to keep the connection alive.\nfunc (ws *WsConn) Ping() error {\n\tws.conn.SetDeadline(time.Now().Add(deadline))\n\treturn ws.conn.WritePing(nil)\n}\n\n// sendMessage encodes data to CBOR and sends it as a binary message to the agent.\n// This is kept for backwards compatibility but new actions should use RequestManager.\nfunc (ws *WsConn) sendMessage(data common.HubRequest[any]) error {\n\tif ws.conn == nil {\n\t\treturn gws.ErrConnClosed\n\t}\n\tbytes, err := cbor.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn ws.conn.WriteMessage(gws.OpcodeBinary, bytes)\n}\n\n// handleAgentRequest processes a request to the agent, handling both legacy and new formats.\nfunc (ws *WsConn) handleAgentRequest(req *PendingRequest, handler ResponseHandler) error {\n\t// Wait for response\n\tselect {\n\tcase message := <-req.ResponseCh:\n\t\tdefer message.Close()\n\t\t// Cancel request context to stop timeout watcher promptly\n\t\tdefer req.Cancel()\n\t\tdata := message.Data.Bytes()\n\n\t\t// Legacy format - unmarshal directly\n\t\tif ws.agentVersion.LT(beszel.MinVersionAgentResponse) {\n\t\t\treturn handler.HandleLegacy(data)\n\t\t}\n\n\t\t// New format with AgentResponse wrapper\n\t\tvar agentResponse common.AgentResponse\n\t\tif err := cbor.Unmarshal(data, &agentResponse); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif agentResponse.Error != \"\" {\n\t\t\treturn errors.New(agentResponse.Error)\n\t\t}\n\t\treturn handler.Handle(agentResponse)\n\n\tcase <-req.Context.Done():\n\t\treturn req.Context.Err()\n\t}\n}\n\n// IsConnected returns true if the WebSocket connection is active.\nfunc (ws *WsConn) IsConnected() bool {\n\treturn ws.conn != nil\n}\n\n// AgentVersion returns the connected agent's version (as reported during handshake).\nfunc (ws *WsConn) AgentVersion() semver.Version {\n\treturn ws.agentVersion\n}\n\n// SendRequest sends a request to the agent and returns a pending request handle.\n// This is used by the transport layer to send requests.\nfunc (ws *WsConn) SendRequest(ctx context.Context, action common.WebSocketAction, data any) (*PendingRequest, error) {\n\treturn ws.requestManager.SendRequest(ctx, action, data)\n}\n"
  },
  {
    "path": "internal/hub/ws/ws_test.go",
    "content": "//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/henrygd/beszel/internal/common\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// TestGetUpgrader tests the singleton upgrader\nfunc TestGetUpgrader(t *testing.T) {\n\t// Reset the global upgrader to test singleton behavior\n\tupgrader = nil\n\n\t// First call should create the upgrader\n\tupgrader1 := GetUpgrader()\n\tassert.NotNil(t, upgrader1, \"Upgrader should not be nil\")\n\n\t// Second call should return the same instance\n\tupgrader2 := GetUpgrader()\n\tassert.Same(t, upgrader1, upgrader2, \"Should return the same upgrader instance\")\n\n\t// Verify it's properly configured\n\tassert.NotNil(t, upgrader1, \"Upgrader should be configured\")\n}\n\n// TestNewWsConnection tests WebSocket connection creation\nfunc TestNewWsConnection(t *testing.T) {\n\t// We can't easily mock gws.Conn, so we'll pass nil and test the structure\n\twsConn := NewWsConnection(nil, semver.MustParse(\"0.12.10\"))\n\n\tassert.NotNil(t, wsConn, \"WebSocket connection should not be nil\")\n\tassert.Nil(t, wsConn.conn, \"Connection should be nil as passed\")\n\tassert.NotNil(t, wsConn.requestManager, \"Request manager should be initialized\")\n\tassert.NotNil(t, wsConn.DownChan, \"Down channel should be initialized\")\n\tassert.Equal(t, 1, cap(wsConn.DownChan), \"Down channel should have capacity of 1\")\n}\n\n// TestWsConn_IsConnected tests the connection status check\nfunc TestWsConn_IsConnected(t *testing.T) {\n\t// Test with nil connection\n\twsConn := NewWsConnection(nil, semver.MustParse(\"0.12.10\"))\n\tassert.False(t, wsConn.IsConnected(), \"Should not be connected when conn is nil\")\n}\n\n// TestWsConn_Close tests the connection closing with nil connection\nfunc TestWsConn_Close(t *testing.T) {\n\twsConn := NewWsConnection(nil, semver.MustParse(\"0.12.10\"))\n\n\t// Should handle nil connection gracefully\n\tassert.NotPanics(t, func() {\n\t\twsConn.Close([]byte(\"test message\"))\n\t}, \"Should not panic when closing nil connection\")\n}\n\n// TestWsConn_SendMessage_CBOR tests CBOR encoding in sendMessage\nfunc TestWsConn_SendMessage_CBOR(t *testing.T) {\n\twsConn := NewWsConnection(nil, semver.MustParse(\"0.12.10\"))\n\n\ttestData := common.HubRequest[any]{\n\t\tAction: common.GetData,\n\t\tData:   \"test data\",\n\t}\n\n\t// This will fail because conn is nil, but we can test the CBOR encoding logic\n\t// by checking that the function properly encodes to CBOR before failing\n\terr := wsConn.sendMessage(testData)\n\tassert.Error(t, err, \"Should error with nil connection\")\n\n\t// Test CBOR encoding separately\n\tbytes, err := cbor.Marshal(testData)\n\tassert.NoError(t, err, \"Should encode to CBOR successfully\")\n\n\t// Verify we can decode it back\n\tvar decodedData common.HubRequest[any]\n\terr = cbor.Unmarshal(bytes, &decodedData)\n\tassert.NoError(t, err, \"Should decode from CBOR successfully\")\n\tassert.Equal(t, testData.Action, decodedData.Action, \"Action should match\")\n}\n\n// TestWsConn_GetFingerprint_SignatureGeneration tests signature creation logic\nfunc TestWsConn_GetFingerprint_SignatureGeneration(t *testing.T) {\n\t// Generate test key pair\n\t_, privKey, err := ed25519.GenerateKey(nil)\n\trequire.NoError(t, err)\n\n\tsigner, err := ssh.NewSignerFromKey(privKey)\n\trequire.NoError(t, err)\n\n\ttoken := \"test-token\"\n\n\t// This will timeout since conn is nil, but we can verify the signature logic\n\t// We can't test the full flow, but we can test that the signature is created properly\n\tchallenge := []byte(token)\n\tsignature, err := signer.Sign(nil, challenge)\n\tassert.NoError(t, err, \"Should create signature successfully\")\n\tassert.NotEmpty(t, signature.Blob, \"Signature blob should not be empty\")\n\tassert.Equal(t, signer.PublicKey().Type(), signature.Format, \"Signature format should match key type\")\n\n\t// Test the fingerprint request structure\n\tfpRequest := common.FingerprintRequest{\n\t\tSignature:   signature.Blob,\n\t\tNeedSysInfo: true,\n\t}\n\n\t// Test CBOR encoding of fingerprint request\n\tfpData, err := cbor.Marshal(fpRequest)\n\tassert.NoError(t, err, \"Should encode fingerprint request to CBOR\")\n\n\tvar decodedFpRequest common.FingerprintRequest\n\terr = cbor.Unmarshal(fpData, &decodedFpRequest)\n\tassert.NoError(t, err, \"Should decode fingerprint request from CBOR\")\n\tassert.Equal(t, fpRequest.Signature, decodedFpRequest.Signature, \"Signature should match\")\n\tassert.Equal(t, fpRequest.NeedSysInfo, decodedFpRequest.NeedSysInfo, \"NeedSysInfo should match\")\n\n\t// Test the full hub request structure\n\thubRequest := common.HubRequest[any]{\n\t\tAction: common.CheckFingerprint,\n\t\tData:   fpRequest,\n\t}\n\n\thubData, err := cbor.Marshal(hubRequest)\n\tassert.NoError(t, err, \"Should encode hub request to CBOR\")\n\n\tvar decodedHubRequest common.HubRequest[cbor.RawMessage]\n\terr = cbor.Unmarshal(hubData, &decodedHubRequest)\n\tassert.NoError(t, err, \"Should decode hub request from CBOR\")\n\tassert.Equal(t, common.CheckFingerprint, decodedHubRequest.Action, \"Action should be CheckFingerprint\")\n}\n\n// TestWsConn_RequestSystemData_RequestFormat tests system data request format\nfunc TestWsConn_RequestSystemData_RequestFormat(t *testing.T) {\n\t// Test the request format that would be sent\n\trequest := common.HubRequest[any]{\n\t\tAction: common.GetData,\n\t}\n\n\t// Test CBOR encoding\n\tdata, err := cbor.Marshal(request)\n\tassert.NoError(t, err, \"Should encode request to CBOR\")\n\n\t// Test decoding\n\tvar decodedRequest common.HubRequest[any]\n\terr = cbor.Unmarshal(data, &decodedRequest)\n\tassert.NoError(t, err, \"Should decode request from CBOR\")\n\tassert.Equal(t, common.GetData, decodedRequest.Action, \"Should have GetData action\")\n}\n\n// TestFingerprintRecord tests the FingerprintRecord struct\nfunc TestFingerprintRecord(t *testing.T) {\n\trecord := FingerprintRecord{\n\t\tId:          \"test-id\",\n\t\tSystemId:    \"system-123\",\n\t\tFingerprint: \"test-fingerprint\",\n\t\tToken:       \"test-token\",\n\t}\n\n\tassert.Equal(t, \"test-id\", record.Id)\n\tassert.Equal(t, \"system-123\", record.SystemId)\n\tassert.Equal(t, \"test-fingerprint\", record.Fingerprint)\n\tassert.Equal(t, \"test-token\", record.Token)\n}\n\n// TestDeadlineConstant tests that the deadline constant is reasonable\nfunc TestDeadlineConstant(t *testing.T) {\n\tassert.Equal(t, 70*time.Second, deadline, \"Deadline should be 70 seconds\")\n}\n\n// TestCommonActions tests that the common actions are properly defined\nfunc TestCommonActions(t *testing.T) {\n\t// Test that the actions we use exist and have expected values\n\tassert.Equal(t, common.WebSocketAction(0), common.GetData, \"GetData should be action 0\")\n\tassert.Equal(t, common.WebSocketAction(1), common.CheckFingerprint, \"CheckFingerprint should be action 1\")\n\tassert.Equal(t, common.WebSocketAction(2), common.GetContainerLogs, \"GetLogs should be action 2\")\n}\n\nfunc TestFingerprintHandler(t *testing.T) {\n\tvar result common.FingerprintResponse\n\th := &fingerprintHandler{result: &result}\n\n\tresp := common.AgentResponse{Fingerprint: &common.FingerprintResponse{\n\t\tFingerprint: \"test-fingerprint\",\n\t\tHostname:    \"test-host\",\n\t}}\n\terr := h.Handle(resp)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test-fingerprint\", result.Fingerprint)\n\tassert.Equal(t, \"test-host\", result.Hostname)\n}\n\n// TestHandler tests that we can create a Handler\nfunc TestHandler(t *testing.T) {\n\thandler := &Handler{}\n\tassert.NotNil(t, handler, \"Handler should be created successfully\")\n\n\t// The Handler embeds gws.BuiltinEventHandler, so it should have the embedded type\n\tassert.NotNil(t, handler.BuiltinEventHandler, \"Should have embedded BuiltinEventHandler\")\n}\n\n// TestWsConnChannelBehavior tests channel behavior without WebSocket connections\nfunc TestWsConnChannelBehavior(t *testing.T) {\n\twsConn := NewWsConnection(nil, semver.MustParse(\"0.12.10\"))\n\n\t// Test that channels are properly initialized and can be used\n\tselect {\n\tcase wsConn.DownChan <- struct{}{}:\n\t\t// Should be able to write to channel\n\tdefault:\n\t\tt.Error(\"Should be able to write to DownChan\")\n\t}\n\n\t// Test reading from DownChan\n\tselect {\n\tcase <-wsConn.DownChan:\n\t\t// Should be able to read from channel\n\tcase <-time.After(10 * time.Millisecond):\n\t\tt.Error(\"Should be able to read from DownChan\")\n\t}\n\n\t// Request manager should have no pending requests initially\n\tassert.Equal(t, 0, wsConn.requestManager.GetPendingCount(), \"Should have no pending requests initially\")\n}\n"
  },
  {
    "path": "internal/hub/ws/ws_test_helpers.go",
    "content": "//go:build testing\n\npackage ws\n\n// GetPendingCount returns the number of pending requests (for monitoring)\nfunc (rm *RequestManager) GetPendingCount() int {\n\trm.RLock()\n\tdefer rm.RUnlock()\n\treturn len(rm.pendingReqs)\n}\n"
  },
  {
    "path": "internal/migrations/0_collections_snapshot_0_19_0_dev_1.go",
    "content": "package migrations\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\t// update collections\n\t\tjsonData := `[\n\t{\n\t\t\"id\": \"elngm8x1l60zi2v\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"viewRule\": \"\",\n\t\t\"createRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"updateRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"deleteRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"name\": \"alerts\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"hn5ly3vi\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"user\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"g5sl3jdg\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"zj3ingrv\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"name\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"Status\",\n\t\t\t\t\t\"CPU\",\n\t\t\t\t\t\"Memory\",\n\t\t\t\t\t\"Disk\",\n\t\t\t\t\t\"Temperature\",\n\t\t\t\t\t\"Bandwidth\",\n\t\t\t\t\t\"GPU\",\n\t\t\t\t\t\"LoadAvg1\",\n\t\t\t\t\t\"LoadAvg5\",\n\t\t\t\t\t\"LoadAvg15\",\n\t\t\t\t\t\"Battery\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"o2ablxvn\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"value\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"fstdehcq\",\n\t\t\t\t\"max\": 60,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"min\",\n\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"6hgdf6hs\",\n\t\t\t\t\"name\": \"triggered\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_MnhEt21L5r` + \"`\" + ` ON ` + \"`\" + `alerts` + \"`\" + ` (\\n  ` + \"`\" + `user` + \"`\" + `,\\n  ` + \"`\" + `system` + \"`\" + `,\\n  ` + \"`\" + `name` + \"`\" + `\\n)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"pbc_1697146157\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"createRule\": null,\n\t\t\"updateRule\": null,\n\t\t\"deleteRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"name\": \"alerts_history\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\"max\": 15,\n\t\t\t\t\t\"min\": 15,\n\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation2375276105\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"user\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\"name\": \"system\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text2466471794\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"alert_id\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text1579384326\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"name\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"number494360628\",\n\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\"name\": \"value\",\n\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\t\"name\": \"created\",\n\t\t\t\t\t\"onCreate\": true,\n\t\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"autodate\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"date2276568630\",\n\t\t\t\t\t\"max\": \"\",\n\t\t\t\t\t\"min\": \"\",\n\t\t\t\t\t\"name\": \"resolved\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"date\"\n\t\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_YdGnup5aqB` + \"`\" + ` ON ` + \"`\" + `alerts_history` + \"`\" + ` (` + \"`\" + `user` + \"`\" + `)\",\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_taLet9VdME` + \"`\" + ` ON ` + \"`\" + `alerts_history` + \"`\" + ` (` + \"`\" + `created` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"juohu4jipgc13v7\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\"\",\n\t\t\"viewRule\": null,\n\t\t\"createRule\": null,\n\t\t\"updateRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"container_stats\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"hutcu6ps\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"r39hhnil\",\n\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\"name\": \"stats\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"json\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"vo7iuj96\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"type\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"1m\",\n\t\t\t\t\t\"10m\",\n\t\t\t\t\t\"20m\",\n\t\t\t\t\t\"120m\",\n\t\t\t\t\t\"480m\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_d87OiXGZD8` + \"`\" + ` ON ` + \"`\" + `container_stats` + \"`\" + ` (\\n  ` + \"`\" + `system` + \"`\" + `,\\n  ` + \"`\" + `type` + \"`\" + `,\\n  ` + \"`\" + `created` + \"`\" + `\\n)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"pbc_3663931638\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"createRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id && @request.auth.role != \\\"readonly\\\"\",\n\t\t\"updateRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id && @request.auth.role != \\\"readonly\\\"\",\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"fingerprints\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{9}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 9,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-zA-Z9-9]{20}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text1597481275\",\n\t\t\t\t\"max\": 255,\n\t\t\t\t\"min\": 9,\n\t\t\t\t\"name\": \"token\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text4228609354\",\n\t\t\t\t\"max\": 255,\n\t\t\t\t\"min\": 9,\n\t\t\t\t\"name\": \"fingerprint\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_p9qZlu26po` + \"`\" + ` ON ` + \"`\" + `fingerprints` + \"`\" + ` (` + \"`\" + `token` + \"`\" + `)\",\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_ngboulGMYw` + \"`\" + ` ON ` + \"`\" + `fingerprints` + \"`\" + ` (` + \"`\" + `system` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"ej9oowivz8b2mht\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\"\",\n\t\t\"viewRule\": null,\n\t\t\"createRule\": null,\n\t\t\"updateRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"system_stats\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"h9sg148r\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"azftn0be\",\n\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\"name\": \"stats\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"json\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"m1ekhli3\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"type\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"1m\",\n\t\t\t\t\t\"10m\",\n\t\t\t\t\t\"20m\",\n\t\t\t\t\t\"120m\",\n\t\t\t\t\t\"480m\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_GxIee0j` + \"`\" + ` ON ` + \"`\" + `system_stats` + \"`\" + ` (\\n  ` + \"`\" + `system` + \"`\" + `,\\n  ` + \"`\" + `type` + \"`\" + `,\\n  ` + \"`\" + `created` + \"`\" + `\\n)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"4afacsdnlu8q8r2\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"viewRule\": null,\n\t\t\"createRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"updateRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"user_settings\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"d5vztyxa\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"user\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"xcx4qgqq\",\n\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\"name\": \"settings\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"json\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_30Lwgf2` + \"`\" + ` ON ` + \"`\" + `user_settings` + \"`\" + ` (` + \"`\" + `user` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"2hz5ncl8tizk5nx\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id\",\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id\",\n\t\t\"createRule\": \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id && @request.auth.role != \\\"readonly\\\"\",\n\t\t\"updateRule\": \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id && @request.auth.role != \\\"readonly\\\"\",\n\t\t\"deleteRule\": \"@request.auth.id != \\\"\\\" && users.id ?= @request.auth.id && @request.auth.role != \\\"readonly\\\"\",\n\t\t\"name\": \"systems\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"7xloxkwk\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"name\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"waj7seaf\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"status\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"up\",\n\t\t\t\t\t\"down\",\n\t\t\t\t\t\"paused\",\n\t\t\t\t\t\"pending\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"ve781smf\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"host\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"pij0k2jk\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"port\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"qoq64ntl\",\n\t\t\t\t\"maxSize\": 2000000,\n\t\t\t\t\"name\": \"info\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"json\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"jcarjnjj\",\n\t\t\t\t\"maxSelect\": 2147483647,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"users\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_systems_status` + \"`\" + ` ON ` + \"`\" + `systems` + \"`\" + ` (` + \"`\" + `status` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"id\": \"_pb_users_auth_\",\n\t\t\"listRule\": \"id = @request.auth.id\",\n\t\t\"viewRule\": \"id = @request.auth.id\",\n\t\t\"createRule\": null,\n\t\t\"updateRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"users\",\n\t\t\"type\": \"auth\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cost\": 10,\n\t\t\t\t\"hidden\": true,\n\t\t\t\t\"id\": \"password901924565\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 8,\n\t\t\t\t\"name\": \"password\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"password\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-zA-Z0-9_]{50}\",\n\t\t\t\t\"hidden\": true,\n\t\t\t\t\"id\": \"text2504183744\",\n\t\t\t\t\"max\": 60,\n\t\t\t\t\"min\": 30,\n\t\t\t\t\"name\": \"tokenKey\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"exceptDomains\": null,\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"email3885137012\",\n\t\t\t\t\"name\": \"email\",\n\t\t\t\t\"onlyDomains\": null,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"email\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"bool1547992806\",\n\t\t\t\t\"name\": \"emailVisibility\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"bool256245529\",\n\t\t\t\t\"name\": \"verified\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"users[0-9]{6}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text4166911607\",\n\t\t\t\t\"max\": 150,\n\t\t\t\t\"min\": 3,\n\t\t\t\t\"name\": \"username\",\n\t\t\t\t\"pattern\": \"^[\\\\w][\\\\w\\\\.\\\\-]*$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"qkbp58ae\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"role\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"user\",\n\t\t\t\t\t\"admin\",\n\t\t\t\t\t\"readonly\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `__pb_users_auth__username_idx` + \"`\" + ` ON ` + \"`\" + `users` + \"`\" + ` (username COLLATE NOCASE)\",\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `__pb_users_auth__email_idx` + \"`\" + ` ON ` + \"`\" + `users` + \"`\" + ` (` + \"`\" + `email` + \"`\" + `) WHERE ` + \"`\" + `email` + \"`\" + ` != ''\",\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `__pb_users_auth__tokenKey_idx` + \"`\" + ` ON ` + \"`\" + `users` + \"`\" + ` (` + \"`\" + `tokenKey` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false,\n\t\t\"authRule\": \"verified=true\",\n\t\t\"manageRule\": null\n\t},\n\t{\n\t\t\"id\": \"pbc_1864144027\",\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"viewRule\": null,\n\t\t\"createRule\": null,\n\t\t\"updateRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"name\": \"containers\",\n\t\t\"type\": \"base\",\n\t\t\"fields\": [\n\t\t\t\t{\n\t\t\t\t\t\t\"autogeneratePattern\": \"[a-f0-9]{6}\",\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\t\t\"max\": 12,\n\t\t\t\t\t\t\"min\": 6,\n\t\t\t\t\t\t\"name\": \"id\",\n\t\t\t\t\t\t\"pattern\": \"^[a-f0-9]+$\",\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\t\"system\": true,\n\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"cascadeDelete\": false,\n\t\t\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\t\t\"name\": \"system\",\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"relation\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"text1579384326\",\n\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\"name\": \"name\",\n\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"text2063623452\",\n\t\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\"name\": \"status\",\n\t\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"number3470402323\",\n\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\t\"name\": \"health\",\n\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"number3128971310\",\n\t\t\t\t\t\t\"max\": 100,\n\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\"name\": \"cpu\",\n\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\t\"id\": \"number3933025333\",\n\t\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\t\"name\": \"memory\",\n\t\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"number4075427327\",\n\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\"name\": \"net\",\n\t\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text3309110367\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"image\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"text2308952269\",\n\t\t\t\t\t\"max\": 0,\n\t\t\t\t\t\"min\": 0,\n\t\t\t\t\t\"name\": \"ports\",\n\t\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\t\"required\": false,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"text\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"hidden\": false,\n\t\t\t\t\t\"id\": \"number3332085495\",\n\t\t\t\t\t\"max\": null,\n\t\t\t\t\t\"min\": null,\n\t\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\t\"presentable\": false,\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"system\": false,\n\t\t\t\t\t\"type\": \"number\"\n\t\t\t\t}\n\t\t],\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_JxWirjdhyO` + \"`\" + ` ON ` + \"`\" + `containers` + \"`\" + ` (` + \"`\" + `updated` + \"`\" + `)\",\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_r3Ja0rs102` + \"`\" + ` ON ` + \"`\" + `containers` + \"`\" + ` (` + \"`\" + `system` + \"`\" + `)\"\n\t\t],\n\t\t\"system\": false\n\t},\n\t{\n\t\t\"createRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{10}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 10,\n\t\t\t\t\"min\": 6,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text1579384326\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"name\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number2063623452\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"state\",\n\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1476559580\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"sub\",\n\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number3128971310\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"cpu\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1052053287\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"cpuPeak\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number3933025333\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"memory\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1828797201\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"memPeak\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number3332085495\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t}\n\t\t],\n\t\t\"id\": \"pbc_3494996990\",\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_4Z7LuLNdQb` + \"`\" + ` ON ` + \"`\" + `systemd_services` + \"`\" + ` (` + \"`\" + `system` + \"`\" + `)\",\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_pBp1fF837e` + \"`\" + ` ON ` + \"`\" + `systemd_services` + \"`\" + ` (` + \"`\" + `updated` + \"`\" + `)\"\n\t\t],\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"name\": \"systemd_services\",\n\t\t\"system\": false,\n\t\t\"type\": \"base\",\n\t\t\"updateRule\": null,\n\t\t\"viewRule\": null\n\t},\n\t{\n\t\t\"createRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"deleteRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{10}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 10,\n\t\t\t\t\"min\": 10,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation2375276105\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"user\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"select2844932856\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"name\": \"type\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"select\",\n\t\t\t\t\"values\": [\n\t\t\t\t\t\"one-time\",\n\t\t\t\t\t\"daily\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"date2675529103\",\n\t\t\t\t\"max\": \"\",\n\t\t\t\t\"min\": \"\",\n\t\t\t\t\"name\": \"start\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"date\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"date16528305\",\n\t\t\t\t\"max\": \"\",\n\t\t\t\t\"min\": \"\",\n\t\t\t\t\"name\": \"end\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"date\"\n\t\t\t}\n\t\t],\n\t\t\"id\": \"pbc_451525641\",\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_q0iKnRP9v8` + \"`\" + ` ON ` + \"`\" + `quiet_hours` + \"`\" + ` (\\n  ` + \"`\" + `user` + \"`\" + `,\\n  ` + \"`\" + `system` + \"`\" + `\\n)\",\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_6T7ljT7FJd` + \"`\" + ` ON ` + \"`\" + `quiet_hours` + \"`\" + ` (\\n  ` + \"`\" + `type` + \"`\" + `,\\n  ` + \"`\" + `end` + \"`\" + `\\n)\"\n\t\t],\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"name\": \"quiet_hours\",\n\t\t\"system\": false,\n\t\t\"type\": \"base\",\n\t\t\"updateRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\",\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && user.id = @request.auth.id\"\n\t},\n\t{\n\t\t\"createRule\": null,\n\t\t\"deleteRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{10}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 10,\n\t\t\t\t\"min\": 10,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text1579384326\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"name\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3616895705\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"model\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text2744374011\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"state\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number3051925876\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"capacity\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number190023114\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"temp\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3589068740\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"firmware\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3547646428\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"serial\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text2363381545\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"type\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1234567890\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"hours\",\n\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number0987654321\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"cycles\",\n\t\t\t\t\"onlyInt\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"json832282224\",\n\t\t\t\t\"maxSize\": 0,\n\t\t\t\t\"name\": \"attributes\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"json\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"id\": \"pbc_2571630677\",\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_DZ9yhvgl44` + \"`\" + ` ON ` + \"`\" + `smart_devices` + \"`\" + ` (` + \"`\" + `system` + \"`\" + `)\"\n\t\t],\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"name\": \"smart_devices\",\n\t\t\"system\": false,\n\t\t\"type\": \"base\",\n\t\t\"updateRule\": null,\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\"\n\t},\n\t{\n\t\t\"createRule\": \"\",\n\t\t\"deleteRule\": \"\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{15}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 15,\n\t\t\t\t\"min\": 15,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"2hz5ncl8tizk5nx\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation3377271179\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"system\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3847340049\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"hostname\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1789936913\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"os\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text2818598173\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"os_name\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text1574083243\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"kernel\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3128971310\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"cpu\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text4161937994\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"arch\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number4245036687\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"cores\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number1871592925\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"threads\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"number3933025333\",\n\t\t\t\t\"max\": null,\n\t\t\t\t\"min\": null,\n\t\t\t\t\"name\": \"memory\",\n\t\t\t\t\"onlyInt\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"number\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"bool2200265312\",\n\t\t\t\t\"name\": \"podman\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"bool\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate3332085495\",\n\t\t\t\t\"name\": \"updated\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": true,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"id\": \"pbc_3116237454\",\n\t\t\"indexes\": [],\n\t\t\"listRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\",\n\t\t\"name\": \"system_details\",\n\t\t\"system\": false,\n\t\t\"type\": \"base\",\n\t\t\"updateRule\": \"\",\n\t\t\"viewRule\": \"@request.auth.id != \\\"\\\" && system.users.id ?= @request.auth.id\"\n\t},\n\t{\n\t\t\"createRule\": null,\n\t\t\"deleteRule\": null,\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"[a-z0-9]{10}\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text3208210256\",\n\t\t\t\t\"max\": 10,\n\t\t\t\t\"min\": 10,\n\t\t\t\t\"name\": \"id\",\n\t\t\t\t\"pattern\": \"^[a-z0-9]+$\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": true,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": true,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"cascadeDelete\": true,\n\t\t\t\t\"collectionId\": \"_pb_users_auth_\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"relation2375276105\",\n\t\t\t\t\"maxSelect\": 1,\n\t\t\t\t\"minSelect\": 0,\n\t\t\t\t\"name\": \"user\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"required\": true,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"relation\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"autogeneratePattern\": \"\",\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"text1597481275\",\n\t\t\t\t\"max\": 0,\n\t\t\t\t\"min\": 0,\n\t\t\t\t\"name\": \"token\",\n\t\t\t\t\"pattern\": \"\",\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"primaryKey\": false,\n\t\t\t\t\"required\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"text\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"hidden\": false,\n\t\t\t\t\"id\": \"autodate2990389176\",\n\t\t\t\t\"name\": \"created\",\n\t\t\t\t\"onCreate\": true,\n\t\t\t\t\"onUpdate\": false,\n\t\t\t\t\"presentable\": false,\n\t\t\t\t\"system\": false,\n\t\t\t\t\"type\": \"autodate\"\n\t\t\t}\n\t\t],\n\t\t\"id\": \"pbc_3383022248\",\n\t\t\"indexes\": [\n\t\t\t\"CREATE INDEX ` + \"`\" + `idx_iaD9Y2Lgbl` + \"`\" + ` ON ` + \"`\" + `universal_tokens` + \"`\" + ` (` + \"`\" + `token` + \"`\" + `)\",\n\t\t\t\"CREATE UNIQUE INDEX ` + \"`\" + `idx_wdR0A4PbRG` + \"`\" + ` ON ` + \"`\" + `universal_tokens` + \"`\" + ` (` + \"`\" + `user` + \"`\" + `)\"\n\t\t],\n\t\t\"listRule\": null,\n\t\t\"name\": \"universal_tokens\",\n\t\t\"system\": false,\n\t\t\"type\": \"base\",\n\t\t\"updateRule\": null,\n\t\t\"viewRule\": null\n\t}\n]`\n\n\t\terr := app.ImportCollectionsByMarshaledJSON([]byte(jsonData), false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}, func(app core.App) error {\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/migrations/initial-settings.go",
    "content": "package migrations\n\nimport (\n\t\"os\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\tm \"github.com/pocketbase/pocketbase/migrations\"\n)\n\nconst (\n\tTempAdminEmail = \"_@b.b\"\n)\n\nfunc init() {\n\tm.Register(func(app core.App) error {\n\t\t// initial settings\n\t\tsettings := app.Settings()\n\t\tsettings.Meta.AppName = \"Beszel\"\n\t\tsettings.Meta.HideControls = true\n\t\tsettings.Logs.MinLevel = 4\n\t\tif err := app.Save(settings); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// create superuser\n\t\tsuperuserCollection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)\n\t\tsuperUser := core.NewRecord(superuserCollection)\n\n\t\t// set email\n\t\temail, _ := GetEnv(\"USER_EMAIL\")\n\t\tpassword, _ := GetEnv(\"USER_PASSWORD\")\n\t\tdidProvideUserDetails := email != \"\" && password != \"\"\n\n\t\t// set superuser email\n\t\tif email == \"\" {\n\t\t\temail = TempAdminEmail\n\t\t}\n\t\tsuperUser.SetEmail(email)\n\n\t\t// set superuser password\n\t\tif password != \"\" {\n\t\t\tsuperUser.SetPassword(password)\n\t\t} else {\n\t\t\tsuperUser.SetRandomPassword()\n\t\t}\n\n\t\t// if user details are provided, we create a regular user as well\n\t\tif didProvideUserDetails {\n\t\t\tusersCollection, _ := app.FindCollectionByNameOrId(\"users\")\n\t\t\tuser := core.NewRecord(usersCollection)\n\t\t\tuser.SetEmail(email)\n\t\t\tuser.SetPassword(password)\n\t\t\tuser.SetVerified(true)\n\t\t\terr := app.Save(user)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn app.Save(superUser)\n\t}, nil)\n}\n\n// GetEnv retrieves an environment variable with a \"BESZEL_HUB_\" prefix, or falls back to the unprefixed key.\nfunc GetEnv(key string) (value string, exists bool) {\n\tif value, exists = os.LookupEnv(\"BESZEL_HUB_\" + key); exists {\n\t\treturn value, exists\n\t}\n\t// Fallback to the old unprefixed key\n\treturn os.LookupEnv(key)\n}\n"
  },
  {
    "path": "internal/records/records.go",
    "content": "// Package records handles creating longer records and deleting old records.\npackage records\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/henrygd/beszel/internal/entities/container\"\n\t\"github.com/henrygd/beszel/internal/entities/system\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype RecordManager struct {\n\tapp core.App\n}\n\ntype LongerRecordData struct {\n\tshorterType        string\n\tlongerType         string\n\tlongerTimeDuration time.Duration\n\tminShorterRecords  int\n}\n\ntype RecordIds []struct {\n\tId string `db:\"id\"`\n}\n\nfunc NewRecordManager(app core.App) *RecordManager {\n\treturn &RecordManager{app}\n}\n\ntype StatsRecord struct {\n\tStats []byte `db:\"stats\"`\n}\n\n// global variables for reusing allocations\nvar (\n\tstatsRecord    StatsRecord\n\tcontainerStats []container.Stats\n\tsumStats       system.Stats\n\ttempStats      system.Stats\n\tqueryParams    = make(dbx.Params, 1)\n\tcontainerSums  = make(map[string]*container.Stats)\n)\n\n// Create longer records by averaging shorter records\nfunc (rm *RecordManager) CreateLongerRecords() {\n\t// start := time.Now()\n\tlongerRecordData := []LongerRecordData{\n\t\t{\n\t\t\tshorterType: \"1m\",\n\t\t\t// change to 9 from 10 to allow edge case timing or short pauses\n\t\t\tminShorterRecords:  9,\n\t\t\tlongerType:         \"10m\",\n\t\t\tlongerTimeDuration: -10 * time.Minute,\n\t\t},\n\t\t{\n\t\t\tshorterType:        \"10m\",\n\t\t\tminShorterRecords:  2,\n\t\t\tlongerType:         \"20m\",\n\t\t\tlongerTimeDuration: -20 * time.Minute,\n\t\t},\n\t\t{\n\t\t\tshorterType:        \"20m\",\n\t\t\tminShorterRecords:  6,\n\t\t\tlongerType:         \"120m\",\n\t\t\tlongerTimeDuration: -120 * time.Minute,\n\t\t},\n\t\t{\n\t\t\tshorterType:        \"120m\",\n\t\t\tminShorterRecords:  4,\n\t\t\tlongerType:         \"480m\",\n\t\t\tlongerTimeDuration: -480 * time.Minute,\n\t\t},\n\t}\n\t// wrap the operations in a transaction\n\trm.app.RunInTransaction(func(txApp core.App) error {\n\t\tvar err error\n\t\tcollections := [2]*core.Collection{}\n\t\tcollections[0], err = txApp.FindCachedCollectionByNameOrId(\"system_stats\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcollections[1], err = txApp.FindCachedCollectionByNameOrId(\"container_stats\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar systems RecordIds\n\t\tdb := txApp.DB()\n\n\t\tdb.NewQuery(\"SELECT id FROM systems WHERE status='up'\").All(&systems)\n\n\t\t// loop through all active systems, time periods, and collections\n\t\tfor _, system := range systems {\n\t\t\t// log.Println(\"processing system\", system.GetString(\"name\"))\n\t\t\tfor i := range longerRecordData {\n\t\t\t\trecordData := longerRecordData[i]\n\t\t\t\t// log.Println(\"processing longer record type\", recordData.longerType)\n\t\t\t\t// add one minute padding for longer records because they are created slightly later than the job start time\n\t\t\t\tlongerRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration + time.Minute)\n\t\t\t\t// shorter records are created independently of longer records, so we shouldn't need to add padding\n\t\t\t\tshorterRecordPeriod := time.Now().UTC().Add(recordData.longerTimeDuration)\n\t\t\t\t// loop through both collections\n\t\t\t\tfor _, collection := range collections {\n\t\t\t\t\t// check creation time of last longer record if not 10m, since 10m is created every run\n\t\t\t\t\tif recordData.longerType != \"10m\" {\n\t\t\t\t\t\tcount, err := txApp.CountRecords(\n\t\t\t\t\t\t\tcollection.Id,\n\t\t\t\t\t\t\tdbx.NewExp(\n\t\t\t\t\t\t\t\t\"system = {:system} AND type = {:type} AND created > {:created}\",\n\t\t\t\t\t\t\t\tdbx.Params{\"type\": recordData.longerType, \"system\": system.Id, \"created\": longerRecordPeriod},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t// continue if longer record exists\n\t\t\t\t\t\tif err != nil || count > 0 {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// get shorter records from the past x minutes\n\t\t\t\t\tvar recordIds RecordIds\n\n\t\t\t\t\terr := txApp.DB().\n\t\t\t\t\t\tSelect(\"id\").\n\t\t\t\t\t\tFrom(collection.Name).\n\t\t\t\t\t\tAndWhere(dbx.NewExp(\n\t\t\t\t\t\t\t\"system={:system} AND type={:type} AND created > {:created}\",\n\t\t\t\t\t\t\tdbx.Params{\n\t\t\t\t\t\t\t\t\"type\":    recordData.shorterType,\n\t\t\t\t\t\t\t\t\"system\":  system.Id,\n\t\t\t\t\t\t\t\t\"created\": shorterRecordPeriod,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)).\n\t\t\t\t\t\tAll(&recordIds)\n\n\t\t\t\t\t// continue if not enough shorter records\n\t\t\t\t\tif err != nil || len(recordIds) < recordData.minShorterRecords {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// average the shorter records and create longer record\n\t\t\t\t\tlongerRecord := core.NewRecord(collection)\n\t\t\t\t\tlongerRecord.Set(\"system\", system.Id)\n\t\t\t\t\tlongerRecord.Set(\"type\", recordData.longerType)\n\t\t\t\t\tswitch collection.Name {\n\t\t\t\t\tcase \"system_stats\":\n\t\t\t\t\t\tlongerRecord.Set(\"stats\", rm.AverageSystemStats(db, recordIds))\n\t\t\t\t\tcase \"container_stats\":\n\n\t\t\t\t\t\tlongerRecord.Set(\"stats\", rm.AverageContainerStats(db, recordIds))\n\t\t\t\t\t}\n\t\t\t\t\tif err := txApp.SaveNoValidate(longerRecord); err != nil {\n\t\t\t\t\t\tlog.Println(\"failed to save longer record\", \"err\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tstatsRecord.Stats = statsRecord.Stats[:0]\n\n\t// log.Println(\"finished creating longer records\", \"time (ms)\", time.Since(start).Milliseconds())\n}\n\n// Calculate the average stats of a list of system_stats records without reflect\nfunc (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *system.Stats {\n\t// Clear/reset global structs for reuse\n\tsumStats = system.Stats{}\n\ttempStats = system.Stats{}\n\tsum := &sumStats\n\tstats := &tempStats\n\t// necessary because uint8 is not big enough for the sum\n\tbatterySum := 0\n\t// accumulate per-core usage across records\n\tvar cpuCoresSums []uint64\n\t// accumulate cpu breakdown [user, system, iowait, steal, idle]\n\tvar cpuBreakdownSums []float64\n\n\tcount := float64(len(records))\n\ttempCount := float64(0)\n\n\t// Accumulate totals\n\tfor _, record := range records {\n\t\tid := record.Id\n\t\t// clear global statsRecord for reuse\n\t\tstatsRecord.Stats = statsRecord.Stats[:0]\n\t\t// reset tempStats each iteration to avoid omitzero fields retaining stale values\n\t\t*stats = system.Stats{}\n\n\t\tqueryParams[\"id\"] = id\n\t\tdb.NewQuery(\"SELECT stats FROM system_stats WHERE id = {:id}\").Bind(queryParams).One(&statsRecord)\n\t\tif err := json.Unmarshal(statsRecord.Stats, stats); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tsum.Cpu += stats.Cpu\n\t\t// accumulate cpu time breakdowns if present\n\t\tif stats.CpuBreakdown != nil {\n\t\t\tif len(cpuBreakdownSums) < len(stats.CpuBreakdown) {\n\t\t\t\tcpuBreakdownSums = append(cpuBreakdownSums, make([]float64, len(stats.CpuBreakdown)-len(cpuBreakdownSums))...)\n\t\t\t}\n\t\t\tfor i, v := range stats.CpuBreakdown {\n\t\t\t\tcpuBreakdownSums[i] += v\n\t\t\t}\n\t\t}\n\t\tsum.Mem += stats.Mem\n\t\tsum.MemUsed += stats.MemUsed\n\t\tsum.MemPct += stats.MemPct\n\t\tsum.MemBuffCache += stats.MemBuffCache\n\t\tsum.MemZfsArc += stats.MemZfsArc\n\t\tsum.Swap += stats.Swap\n\t\tsum.SwapUsed += stats.SwapUsed\n\t\tsum.DiskTotal += stats.DiskTotal\n\t\tsum.DiskUsed += stats.DiskUsed\n\t\tsum.DiskPct += stats.DiskPct\n\t\tsum.DiskReadPs += stats.DiskReadPs\n\t\tsum.DiskWritePs += stats.DiskWritePs\n\t\tsum.NetworkSent += stats.NetworkSent\n\t\tsum.NetworkRecv += stats.NetworkRecv\n\t\tsum.LoadAvg[0] += stats.LoadAvg[0]\n\t\tsum.LoadAvg[1] += stats.LoadAvg[1]\n\t\tsum.LoadAvg[2] += stats.LoadAvg[2]\n\t\tsum.Bandwidth[0] += stats.Bandwidth[0]\n\t\tsum.Bandwidth[1] += stats.Bandwidth[1]\n\t\tsum.DiskIO[0] += stats.DiskIO[0]\n\t\tsum.DiskIO[1] += stats.DiskIO[1]\n\t\tbatterySum += int(stats.Battery[0])\n\t\tsum.Battery[1] = stats.Battery[1]\n\n\t\t// accumulate per-core usage if present\n\t\tif stats.CpuCoresUsage != nil {\n\t\t\tif len(cpuCoresSums) < len(stats.CpuCoresUsage) {\n\t\t\t\t// extend slices to accommodate core count\n\t\t\t\tcpuCoresSums = append(cpuCoresSums, make([]uint64, len(stats.CpuCoresUsage)-len(cpuCoresSums))...)\n\t\t\t}\n\t\t\tfor i, v := range stats.CpuCoresUsage {\n\t\t\t\tcpuCoresSums[i] += uint64(v)\n\t\t\t}\n\t\t}\n\t\t// Set peak values\n\t\tsum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)\n\t\tsum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)\n\t\tsum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)\n\t\tsum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)\n\t\tsum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)\n\t\tsum.MaxDiskWritePs = max(sum.MaxDiskWritePs, stats.MaxDiskWritePs, stats.DiskWritePs)\n\t\tsum.MaxBandwidth[0] = max(sum.MaxBandwidth[0], stats.MaxBandwidth[0], stats.Bandwidth[0])\n\t\tsum.MaxBandwidth[1] = max(sum.MaxBandwidth[1], stats.MaxBandwidth[1], stats.Bandwidth[1])\n\t\tsum.MaxDiskIO[0] = max(sum.MaxDiskIO[0], stats.MaxDiskIO[0], stats.DiskIO[0])\n\t\tsum.MaxDiskIO[1] = max(sum.MaxDiskIO[1], stats.MaxDiskIO[1], stats.DiskIO[1])\n\n\t\t// Accumulate network interfaces\n\t\tif sum.NetworkInterfaces == nil {\n\t\t\tsum.NetworkInterfaces = make(map[string][4]uint64, len(stats.NetworkInterfaces))\n\t\t}\n\t\tfor key, value := range stats.NetworkInterfaces {\n\t\t\tsum.NetworkInterfaces[key] = [4]uint64{\n\t\t\t\tsum.NetworkInterfaces[key][0] + value[0],\n\t\t\t\tsum.NetworkInterfaces[key][1] + value[1],\n\t\t\t\tmax(sum.NetworkInterfaces[key][2], value[2]),\n\t\t\t\tmax(sum.NetworkInterfaces[key][3], value[3]),\n\t\t\t}\n\t\t}\n\n\t\t// Accumulate temperatures\n\t\tif stats.Temperatures != nil {\n\t\t\tif sum.Temperatures == nil {\n\t\t\t\tsum.Temperatures = make(map[string]float64, len(stats.Temperatures))\n\t\t\t}\n\t\t\ttempCount++\n\t\t\tfor key, value := range stats.Temperatures {\n\t\t\t\tsum.Temperatures[key] += value\n\t\t\t}\n\t\t}\n\n\t\t// Accumulate extra filesystem stats\n\t\tif stats.ExtraFs != nil {\n\t\t\tif sum.ExtraFs == nil {\n\t\t\t\tsum.ExtraFs = make(map[string]*system.FsStats, len(stats.ExtraFs))\n\t\t\t}\n\t\t\tfor key, value := range stats.ExtraFs {\n\t\t\t\tif _, ok := sum.ExtraFs[key]; !ok {\n\t\t\t\t\tsum.ExtraFs[key] = &system.FsStats{}\n\t\t\t\t}\n\t\t\t\tfs := sum.ExtraFs[key]\n\t\t\t\tfs.DiskTotal += value.DiskTotal\n\t\t\t\tfs.DiskUsed += value.DiskUsed\n\t\t\t\tfs.DiskWritePs += value.DiskWritePs\n\t\t\t\tfs.DiskReadPs += value.DiskReadPs\n\t\t\t\tfs.MaxDiskReadPS = max(fs.MaxDiskReadPS, value.MaxDiskReadPS, value.DiskReadPs)\n\t\t\t\tfs.MaxDiskWritePS = max(fs.MaxDiskWritePS, value.MaxDiskWritePS, value.DiskWritePs)\n\t\t\t\tfs.DiskReadBytes += value.DiskReadBytes\n\t\t\t\tfs.DiskWriteBytes += value.DiskWriteBytes\n\t\t\t\tfs.MaxDiskReadBytes = max(fs.MaxDiskReadBytes, value.MaxDiskReadBytes, value.DiskReadBytes)\n\t\t\t\tfs.MaxDiskWriteBytes = max(fs.MaxDiskWriteBytes, value.MaxDiskWriteBytes, value.DiskWriteBytes)\n\t\t\t}\n\t\t}\n\n\t\t// Accumulate GPU data\n\t\tif stats.GPUData != nil {\n\t\t\tif sum.GPUData == nil {\n\t\t\t\tsum.GPUData = make(map[string]system.GPUData, len(stats.GPUData))\n\t\t\t}\n\t\t\tfor id, value := range stats.GPUData {\n\t\t\t\tgpu, ok := sum.GPUData[id]\n\t\t\t\tif !ok {\n\t\t\t\t\tgpu = system.GPUData{Name: value.Name}\n\t\t\t\t}\n\t\t\t\tgpu.Temperature += value.Temperature\n\t\t\t\tgpu.MemoryUsed += value.MemoryUsed\n\t\t\t\tgpu.MemoryTotal += value.MemoryTotal\n\t\t\t\tgpu.Usage += value.Usage\n\t\t\t\tgpu.Power += value.Power\n\t\t\t\tgpu.Count += value.Count\n\n\t\t\t\tif value.Engines != nil {\n\t\t\t\t\tif gpu.Engines == nil {\n\t\t\t\t\t\tgpu.Engines = make(map[string]float64, len(value.Engines))\n\t\t\t\t\t}\n\t\t\t\t\tfor engineKey, engineValue := range value.Engines {\n\t\t\t\t\t\tgpu.Engines[engineKey] += engineValue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsum.GPUData[id] = gpu\n\t\t\t}\n\t\t}\n\t}\n\n\t// Compute averages in place\n\tif count > 0 {\n\t\tsum.Cpu = twoDecimals(sum.Cpu / count)\n\t\tsum.Mem = twoDecimals(sum.Mem / count)\n\t\tsum.MemUsed = twoDecimals(sum.MemUsed / count)\n\t\tsum.MemPct = twoDecimals(sum.MemPct / count)\n\t\tsum.MemBuffCache = twoDecimals(sum.MemBuffCache / count)\n\t\tsum.MemZfsArc = twoDecimals(sum.MemZfsArc / count)\n\t\tsum.Swap = twoDecimals(sum.Swap / count)\n\t\tsum.SwapUsed = twoDecimals(sum.SwapUsed / count)\n\t\tsum.DiskTotal = twoDecimals(sum.DiskTotal / count)\n\t\tsum.DiskUsed = twoDecimals(sum.DiskUsed / count)\n\t\tsum.DiskPct = twoDecimals(sum.DiskPct / count)\n\t\tsum.DiskReadPs = twoDecimals(sum.DiskReadPs / count)\n\t\tsum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)\n\t\tsum.DiskIO[0] = sum.DiskIO[0] / uint64(count)\n\t\tsum.DiskIO[1] = sum.DiskIO[1] / uint64(count)\n\t\tsum.NetworkSent = twoDecimals(sum.NetworkSent / count)\n\t\tsum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)\n\t\tsum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)\n\t\tsum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)\n\t\tsum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)\n\t\tsum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)\n\t\tsum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)\n\t\tsum.Battery[0] = uint8(batterySum / int(count))\n\n\t\t// Average network interfaces\n\t\tif sum.NetworkInterfaces != nil {\n\t\t\tfor key := range sum.NetworkInterfaces {\n\t\t\t\tsum.NetworkInterfaces[key] = [4]uint64{\n\t\t\t\t\tsum.NetworkInterfaces[key][0] / uint64(count),\n\t\t\t\t\tsum.NetworkInterfaces[key][1] / uint64(count),\n\t\t\t\t\tsum.NetworkInterfaces[key][2],\n\t\t\t\t\tsum.NetworkInterfaces[key][3],\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Average temperatures\n\t\tif sum.Temperatures != nil && tempCount > 0 {\n\t\t\tfor key := range sum.Temperatures {\n\t\t\t\tsum.Temperatures[key] = twoDecimals(sum.Temperatures[key] / tempCount)\n\t\t\t}\n\t\t}\n\n\t\t// Average extra filesystem stats\n\t\tif sum.ExtraFs != nil {\n\t\t\tfor key := range sum.ExtraFs {\n\t\t\t\tfs := sum.ExtraFs[key]\n\t\t\t\tfs.DiskTotal = twoDecimals(fs.DiskTotal / count)\n\t\t\t\tfs.DiskUsed = twoDecimals(fs.DiskUsed / count)\n\t\t\t\tfs.DiskWritePs = twoDecimals(fs.DiskWritePs / count)\n\t\t\t\tfs.DiskReadPs = twoDecimals(fs.DiskReadPs / count)\n\t\t\t\tfs.DiskReadBytes = fs.DiskReadBytes / uint64(count)\n\t\t\t\tfs.DiskWriteBytes = fs.DiskWriteBytes / uint64(count)\n\t\t\t}\n\t\t}\n\n\t\t// Average GPU data\n\t\tif sum.GPUData != nil {\n\t\t\tfor id := range sum.GPUData {\n\t\t\t\tgpu := sum.GPUData[id]\n\t\t\t\tgpu.Temperature = twoDecimals(gpu.Temperature / count)\n\t\t\t\tgpu.MemoryUsed = twoDecimals(gpu.MemoryUsed / count)\n\t\t\t\tgpu.MemoryTotal = twoDecimals(gpu.MemoryTotal / count)\n\t\t\t\tgpu.Usage = twoDecimals(gpu.Usage / count)\n\t\t\t\tgpu.Power = twoDecimals(gpu.Power / count)\n\t\t\t\tgpu.Count = twoDecimals(gpu.Count / count)\n\n\t\t\t\tif gpu.Engines != nil {\n\t\t\t\t\tfor engineKey := range gpu.Engines {\n\t\t\t\t\t\tgpu.Engines[engineKey] = twoDecimals(gpu.Engines[engineKey] / count)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsum.GPUData[id] = gpu\n\t\t\t}\n\t\t}\n\n\t\t// Average per-core usage\n\t\tif len(cpuCoresSums) > 0 {\n\t\t\tavg := make(system.Uint8Slice, len(cpuCoresSums))\n\t\t\tfor i := range cpuCoresSums {\n\t\t\t\tv := math.Round(float64(cpuCoresSums[i]) / count)\n\t\t\t\tavg[i] = uint8(v)\n\t\t\t}\n\t\t\tsum.CpuCoresUsage = avg\n\t\t}\n\n\t\t// Average CPU breakdown\n\t\tif len(cpuBreakdownSums) > 0 {\n\t\t\tavg := make([]float64, len(cpuBreakdownSums))\n\t\t\tfor i := range cpuBreakdownSums {\n\t\t\t\tavg[i] = twoDecimals(cpuBreakdownSums[i] / count)\n\t\t\t}\n\t\t\tsum.CpuBreakdown = avg\n\t\t}\n\t}\n\n\treturn sum\n}\n\n// Calculate the average stats of a list of container_stats records\nfunc (rm *RecordManager) AverageContainerStats(db dbx.Builder, records RecordIds) []container.Stats {\n\t// Clear global map for reuse\n\tfor k := range containerSums {\n\t\tdelete(containerSums, k)\n\t}\n\tsums := containerSums\n\tcount := float64(len(records))\n\n\tfor i := range records {\n\t\tid := records[i].Id\n\t\t// clear global statsRecord for reuse\n\t\tstatsRecord.Stats = statsRecord.Stats[:0]\n\t\t// must set to nil (not [:0]) to avoid json.Unmarshal reusing backing array\n\t\t// which causes omitzero fields to inherit stale values from previous iterations\n\t\tcontainerStats = nil\n\n\t\tqueryParams[\"id\"] = id\n\t\tdb.NewQuery(\"SELECT stats FROM container_stats WHERE id = {:id}\").Bind(queryParams).One(&statsRecord)\n\n\t\tif err := json.Unmarshal(statsRecord.Stats, &containerStats); err != nil {\n\t\t\treturn []container.Stats{}\n\t\t}\n\t\tfor i := range containerStats {\n\t\t\tstat := containerStats[i]\n\t\t\tif _, ok := sums[stat.Name]; !ok {\n\t\t\t\tsums[stat.Name] = &container.Stats{Name: stat.Name}\n\t\t\t}\n\t\t\tsums[stat.Name].Cpu += stat.Cpu\n\t\t\tsums[stat.Name].Mem += stat.Mem\n\t\t\tsentBytes := stat.Bandwidth[0]\n\t\t\trecvBytes := stat.Bandwidth[1]\n\t\t\tif sentBytes == 0 && recvBytes == 0 && (stat.NetworkSent != 0 || stat.NetworkRecv != 0) {\n\t\t\t\tsentBytes = uint64(stat.NetworkSent * 1024 * 1024)\n\t\t\t\trecvBytes = uint64(stat.NetworkRecv * 1024 * 1024)\n\t\t\t}\n\t\t\tsums[stat.Name].Bandwidth[0] += sentBytes\n\t\t\tsums[stat.Name].Bandwidth[1] += recvBytes\n\t\t}\n\t}\n\n\tresult := make([]container.Stats, 0, len(sums))\n\tfor _, value := range sums {\n\t\tresult = append(result, container.Stats{\n\t\t\tName:      value.Name,\n\t\t\tCpu:       twoDecimals(value.Cpu / count),\n\t\t\tMem:       twoDecimals(value.Mem / count),\n\t\t\tBandwidth: [2]uint64{uint64(float64(value.Bandwidth[0]) / count), uint64(float64(value.Bandwidth[1]) / count)},\n\t\t})\n\t}\n\treturn result\n}\n\n// Delete old records\nfunc (rm *RecordManager) DeleteOldRecords() {\n\trm.app.RunInTransaction(func(txApp core.App) error {\n\t\terr := deleteOldSystemStats(txApp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = deleteOldContainerRecords(txApp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = deleteOldSystemdServiceRecords(txApp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = deleteOldAlertsHistory(txApp, 200, 250)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = deleteOldQuietHours(txApp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// Delete old alerts history records\nfunc deleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {\n\tdb := app.DB()\n\tvar users []struct {\n\t\tId string `db:\"user\"`\n\t}\n\terr := db.NewQuery(\"SELECT user, COUNT(*) as count FROM alerts_history GROUP BY user HAVING count > {:countBeforeDeletion}\").Bind(dbx.Params{\"countBeforeDeletion\": countBeforeDeletion}).All(&users)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, user := range users {\n\t\t_, err = db.NewQuery(\"DELETE FROM alerts_history WHERE user = {:user} AND id NOT IN (SELECT id FROM alerts_history WHERE user = {:user} ORDER BY created DESC LIMIT {:countToKeep})\").Bind(dbx.Params{\"user\": user.Id, \"countToKeep\": countToKeep}).Execute()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Deletes system_stats records older than what is displayed in the UI\nfunc deleteOldSystemStats(app core.App) error {\n\t// Collections to process\n\tcollections := [2]string{\"system_stats\", \"container_stats\"}\n\n\t// Record types and their retention periods\n\ttype RecordDeletionData struct {\n\t\trecordType string\n\t\tretention  time.Duration\n\t}\n\trecordData := []RecordDeletionData{\n\t\t{recordType: \"1m\", retention: time.Hour},             // 1 hour\n\t\t{recordType: \"10m\", retention: 12 * time.Hour},       // 12 hours\n\t\t{recordType: \"20m\", retention: 24 * time.Hour},       // 1 day\n\t\t{recordType: \"120m\", retention: 7 * 24 * time.Hour},  // 7 days\n\t\t{recordType: \"480m\", retention: 30 * 24 * time.Hour}, // 30 days\n\t}\n\n\tnow := time.Now().UTC()\n\n\tfor _, collection := range collections {\n\t\t// Build the WHERE clause\n\t\tvar conditionParts []string\n\t\tvar params dbx.Params = make(map[string]any)\n\t\tfor i := range recordData {\n\t\t\trd := recordData[i]\n\t\t\t// Create parameterized condition for this record type\n\t\t\tdateParam := fmt.Sprintf(\"date%d\", i)\n\t\t\tconditionParts = append(conditionParts, fmt.Sprintf(\"(type = '%s' AND created < {:%s})\", rd.recordType, dateParam))\n\t\t\tparams[dateParam] = now.Add(-rd.retention)\n\t\t}\n\t\t// Combine conditions with OR\n\t\tconditionStr := strings.Join(conditionParts, \" OR \")\n\t\t// Construct and execute the full raw query\n\t\trawQuery := fmt.Sprintf(\"DELETE FROM %s WHERE %s\", collection, conditionStr)\n\t\tif _, err := app.DB().NewQuery(rawQuery).Bind(params).Execute(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete from %s: %v\", collection, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Deletes systemd service records that haven't been updated in the last 20 minutes\nfunc deleteOldSystemdServiceRecords(app core.App) error {\n\tnow := time.Now().UTC()\n\ttwentyMinutesAgo := now.Add(-20 * time.Minute)\n\n\t// Delete systemd service records where updated < twentyMinutesAgo\n\t_, err := app.DB().NewQuery(\"DELETE FROM systemd_services WHERE updated < {:updated}\").Bind(dbx.Params{\"updated\": twentyMinutesAgo.UnixMilli()}).Execute()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete old systemd service records: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// Deletes container records that haven't been updated in the last 10 minutes\nfunc deleteOldContainerRecords(app core.App) error {\n\tnow := time.Now().UTC()\n\ttenMinutesAgo := now.Add(-10 * time.Minute)\n\n\t// Delete container records where updated < tenMinutesAgo\n\t_, err := app.DB().NewQuery(\"DELETE FROM containers WHERE updated < {:updated}\").Bind(dbx.Params{\"updated\": tenMinutesAgo.UnixMilli()}).Execute()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete old container records: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// Deletes old quiet hours records where end date has passed\nfunc deleteOldQuietHours(app core.App) error {\n\tnow := time.Now().UTC()\n\t_, err := app.DB().NewQuery(\"DELETE FROM quiet_hours WHERE type = 'one-time' AND end < {:now}\").Bind(dbx.Params{\"now\": now}).Execute()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n/* Round float to two decimals */\nfunc twoDecimals(value float64) float64 {\n\treturn math.Round(value*100) / 100\n}\n"
  },
  {
    "path": "internal/records/records_test.go",
    "content": "//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/records\"\n\t\"github.com/henrygd/beszel/internal/tests\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tools/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestDeleteOldRecords tests the main DeleteOldRecords function\nfunc TestDeleteOldRecords(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\trm := records.NewRecordManager(hub)\n\n\t// Create test user for alerts history\n\tuser, err := tests.CreateUser(hub, \"test@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\t// Create test system\n\tsystem, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"up\",\n\t\t\"users\":  []string{user.Id},\n\t})\n\trequire.NoError(t, err)\n\n\tnow := time.Now()\n\n\t// Create old system_stats records that should be deleted\n\tvar record *core.Record\n\trecord, err = tests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\"system\": system.Id,\n\t\t\"type\":   \"1m\",\n\t\t\"stats\":  `{\"cpu\": 50.0, \"mem\": 1024}`,\n\t})\n\trequire.NoError(t, err)\n\t// created is autodate field, so we need to set it manually\n\trecord.SetRaw(\"created\", now.UTC().Add(-2*time.Hour).Format(types.DefaultDateLayout))\n\terr = hub.SaveNoValidate(record)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, record)\n\trequire.InDelta(t, record.GetDateTime(\"created\").Time().UTC().Unix(), now.UTC().Add(-2*time.Hour).Unix(), 1)\n\trequire.Equal(t, record.Get(\"system\"), system.Id)\n\trequire.Equal(t, record.Get(\"type\"), \"1m\")\n\n\t// Create recent system_stats record that should be kept\n\t_, err = tests.CreateRecord(hub, \"system_stats\", map[string]any{\n\t\t\"system\":  system.Id,\n\t\t\"type\":    \"1m\",\n\t\t\"stats\":   `{\"cpu\": 30.0, \"mem\": 512}`,\n\t\t\"created\": now.Add(-30 * time.Minute), // 30 minutes old, should be kept\n\t})\n\trequire.NoError(t, err)\n\n\t// Create many alerts history records to trigger deletion\n\tfor i := range 260 { // More than countBeforeDeletion (250)\n\t\t_, err = tests.CreateRecord(hub, \"alerts_history\", map[string]any{\n\t\t\t\"user\":    user.Id,\n\t\t\t\"name\":    \"CPU\",\n\t\t\t\"value\":   i + 1,\n\t\t\t\"system\":  system.Id,\n\t\t\t\"created\": now.Add(-time.Duration(i) * time.Minute),\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Count records before deletion\n\tsystemStatsCountBefore, err := hub.CountRecords(\"system_stats\")\n\trequire.NoError(t, err)\n\talertsCountBefore, err := hub.CountRecords(\"alerts_history\")\n\trequire.NoError(t, err)\n\n\t// Run deletion\n\trm.DeleteOldRecords()\n\n\t// Count records after deletion\n\tsystemStatsCountAfter, err := hub.CountRecords(\"system_stats\")\n\trequire.NoError(t, err)\n\talertsCountAfter, err := hub.CountRecords(\"alerts_history\")\n\trequire.NoError(t, err)\n\n\t// Verify old system stats were deleted\n\tassert.Less(t, systemStatsCountAfter, systemStatsCountBefore, \"Old system stats should be deleted\")\n\n\t// Verify alerts history was trimmed\n\tassert.Less(t, alertsCountAfter, alertsCountBefore, \"Excessive alerts history should be deleted\")\n\tassert.Equal(t, alertsCountAfter, int64(200), \"Alerts count should be equal to countToKeep (200)\")\n}\n\n// TestDeleteOldSystemStats tests the deleteOldSystemStats function\nfunc TestDeleteOldSystemStats(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\t// Create test system\n\tuser, err := tests.CreateUser(hub, \"test@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tsystem, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"up\",\n\t\t\"users\":  []string{user.Id},\n\t})\n\trequire.NoError(t, err)\n\n\tnow := time.Now().UTC()\n\n\t// Test data for different record types and their retention periods\n\ttestCases := []struct {\n\t\trecordType   string\n\t\tretention    time.Duration\n\t\tshouldBeKept bool\n\t\tageFromNow   time.Duration\n\t\tdescription  string\n\t}{\n\t\t{\"1m\", time.Hour, true, 30 * time.Minute, \"1m record within 1 hour should be kept\"},\n\t\t{\"1m\", time.Hour, false, 2 * time.Hour, \"1m record older than 1 hour should be deleted\"},\n\t\t{\"10m\", 12 * time.Hour, true, 6 * time.Hour, \"10m record within 12 hours should be kept\"},\n\t\t{\"10m\", 12 * time.Hour, false, 24 * time.Hour, \"10m record older than 12 hours should be deleted\"},\n\t\t{\"20m\", 24 * time.Hour, true, 12 * time.Hour, \"20m record within 24 hours should be kept\"},\n\t\t{\"20m\", 24 * time.Hour, false, 48 * time.Hour, \"20m record older than 24 hours should be deleted\"},\n\t\t{\"120m\", 7 * 24 * time.Hour, true, 3 * 24 * time.Hour, \"120m record within 7 days should be kept\"},\n\t\t{\"120m\", 7 * 24 * time.Hour, false, 10 * 24 * time.Hour, \"120m record older than 7 days should be deleted\"},\n\t\t{\"480m\", 30 * 24 * time.Hour, true, 15 * 24 * time.Hour, \"480m record within 30 days should be kept\"},\n\t\t{\"480m\", 30 * 24 * time.Hour, false, 45 * 24 * time.Hour, \"480m record older than 30 days should be deleted\"},\n\t}\n\n\t// Create test records for both system_stats and container_stats\n\tcollections := []string{\"system_stats\", \"container_stats\"}\n\trecordIds := make(map[string][]string)\n\n\tfor _, collection := range collections {\n\t\trecordIds[collection] = make([]string, 0)\n\n\t\tfor i, tc := range testCases {\n\t\t\trecordTime := now.Add(-tc.ageFromNow)\n\n\t\t\tvar stats string\n\t\t\tif collection == \"system_stats\" {\n\t\t\t\tstats = fmt.Sprintf(`{\"cpu\": %d.0, \"mem\": %d}`, i*10, i*100)\n\t\t\t} else {\n\t\t\t\tstats = fmt.Sprintf(`[{\"name\": \"container%d\", \"cpu\": %d.0, \"mem\": %d}]`, i, i*5, i*50)\n\t\t\t}\n\n\t\t\trecord, err := tests.CreateRecord(hub, collection, map[string]any{\n\t\t\t\t\"system\": system.Id,\n\t\t\t\t\"type\":   tc.recordType,\n\t\t\t\t\"stats\":  stats,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\trecord.SetRaw(\"created\", recordTime.Format(types.DefaultDateLayout))\n\t\t\terr = hub.SaveNoValidate(record)\n\t\t\trequire.NoError(t, err)\n\t\t\trecordIds[collection] = append(recordIds[collection], record.Id)\n\t\t}\n\t}\n\n\t// Run deletion\n\terr = records.DeleteOldSystemStats(hub)\n\trequire.NoError(t, err)\n\n\t// Verify results\n\tfor _, collection := range collections {\n\t\tfor i, tc := range testCases {\n\t\t\trecordId := recordIds[collection][i]\n\n\t\t\t// Try to find the record\n\t\t\t_, err := hub.FindRecordById(collection, recordId)\n\n\t\t\tif tc.shouldBeKept {\n\t\t\t\tassert.NoError(t, err, \"Record should exist: %s\", tc.description)\n\t\t\t} else {\n\t\t\t\tassert.Error(t, err, \"Record should be deleted: %s\", tc.description)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// TestDeleteOldAlertsHistory tests the deleteOldAlertsHistory function\nfunc TestDeleteOldAlertsHistory(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\t// Create test users\n\tuser1, err := tests.CreateUser(hub, \"user1@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tuser2, err := tests.CreateUser(hub, \"user2@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tsystem, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"up\",\n\t\t\"users\":  []string{user1.Id, user2.Id},\n\t})\n\trequire.NoError(t, err)\n\tnow := time.Now().UTC()\n\n\ttestCases := []struct {\n\t\tname                  string\n\t\tuser                  *core.Record\n\t\talertCount            int\n\t\tcountToKeep           int\n\t\tcountBeforeDeletion   int\n\t\texpectedAfterDeletion int\n\t\tdescription           string\n\t}{\n\t\t{\n\t\t\tname:                  \"User with few alerts (below threshold)\",\n\t\t\tuser:                  user1,\n\t\t\talertCount:            100,\n\t\t\tcountToKeep:           50,\n\t\t\tcountBeforeDeletion:   150,\n\t\t\texpectedAfterDeletion: 100, // No deletion because below threshold\n\t\t\tdescription:           \"User with alerts below countBeforeDeletion should not have any deleted\",\n\t\t},\n\t\t{\n\t\t\tname:                  \"User with many alerts (above threshold)\",\n\t\t\tuser:                  user2,\n\t\t\talertCount:            300,\n\t\t\tcountToKeep:           100,\n\t\t\tcountBeforeDeletion:   200,\n\t\t\texpectedAfterDeletion: 100, // Should be trimmed to countToKeep\n\t\t\tdescription:           \"User with alerts above countBeforeDeletion should be trimmed to countToKeep\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Create alerts for this user\n\t\t\tfor i := 0; i < tc.alertCount; i++ {\n\t\t\t\t_, err := tests.CreateRecord(hub, \"alerts_history\", map[string]any{\n\t\t\t\t\t\"user\":    tc.user.Id,\n\t\t\t\t\t\"name\":    \"CPU\",\n\t\t\t\t\t\"value\":   i + 1,\n\t\t\t\t\t\"system\":  system.Id,\n\t\t\t\t\t\"created\": now.Add(-time.Duration(i) * time.Minute),\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Count before deletion\n\t\t\tcountBefore, err := hub.CountRecords(\"alerts_history\",\n\t\t\t\tdbx.NewExp(\"user = {:user}\", dbx.Params{\"user\": tc.user.Id}))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, int64(tc.alertCount), countBefore, \"Initial count should match\")\n\n\t\t\t// Run deletion\n\t\t\terr = records.DeleteOldAlertsHistory(hub, tc.countToKeep, tc.countBeforeDeletion)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Count after deletion\n\t\t\tcountAfter, err := hub.CountRecords(\"alerts_history\",\n\t\t\t\tdbx.NewExp(\"user = {:user}\", dbx.Params{\"user\": tc.user.Id}))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, int64(tc.expectedAfterDeletion), countAfter, tc.description)\n\n\t\t\t// If deletion occurred, verify the most recent records were kept\n\t\t\tif tc.expectedAfterDeletion < tc.alertCount {\n\t\t\t\trecords, err := hub.FindRecordsByFilter(\"alerts_history\",\n\t\t\t\t\t\"user = {:user}\",\n\t\t\t\t\t\"-created\", // Order by created DESC\n\t\t\t\t\ttc.countToKeep,\n\t\t\t\t\t0,\n\t\t\t\t\tmap[string]any{\"user\": tc.user.Id})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Len(t, records, tc.expectedAfterDeletion, \"Should have exactly countToKeep records\")\n\n\t\t\t\t// Verify records are in descending order by created time\n\t\t\t\tfor i := 1; i < len(records); i++ {\n\t\t\t\t\tprev := records[i-1].GetDateTime(\"created\").Time()\n\t\t\t\t\tcurr := records[i].GetDateTime(\"created\").Time()\n\t\t\t\t\tassert.True(t, prev.After(curr) || prev.Equal(curr),\n\t\t\t\t\t\t\"Records should be ordered by created time (newest first)\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestDeleteOldAlertsHistoryEdgeCases tests edge cases for alerts history deletion\nfunc TestDeleteOldAlertsHistoryEdgeCases(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\tt.Run(\"No users with excessive alerts\", func(t *testing.T) {\n\t\t// Create user with few alerts\n\t\tuser, err := tests.CreateUser(hub, \"few@example.com\", \"testtesttest\")\n\t\trequire.NoError(t, err)\n\n\t\tsystem, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\t\"name\":   \"test-system\",\n\t\t\t\"host\":   \"localhost\",\n\t\t\t\"port\":   \"45876\",\n\t\t\t\"status\": \"up\",\n\t\t\t\"users\":  []string{user.Id},\n\t\t})\n\n\t\t// Create only 5 alerts (well below threshold)\n\t\tfor i := range 5 {\n\t\t\t_, err := tests.CreateRecord(hub, \"alerts_history\", map[string]any{\n\t\t\t\t\"user\":   user.Id,\n\t\t\t\t\"name\":   \"CPU\",\n\t\t\t\t\"value\":  i + 1,\n\t\t\t\t\"system\": system.Id,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\t// Should not error and should not delete anything\n\t\terr = records.DeleteOldAlertsHistory(hub, 10, 20)\n\t\trequire.NoError(t, err)\n\n\t\tcount, err := hub.CountRecords(\"alerts_history\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, int64(5), count, \"All alerts should remain\")\n\t})\n\n\tt.Run(\"Empty alerts_history table\", func(t *testing.T) {\n\t\t// Clear any existing alerts\n\t\t_, err := hub.DB().NewQuery(\"DELETE FROM alerts_history\").Execute()\n\t\trequire.NoError(t, err)\n\n\t\t// Should not error with empty table\n\t\terr = records.DeleteOldAlertsHistory(hub, 10, 20)\n\t\trequire.NoError(t, err)\n\t})\n}\n\n// TestDeleteOldSystemdServiceRecords tests systemd service cleanup via DeleteOldRecords\nfunc TestDeleteOldSystemdServiceRecords(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\trm := records.NewRecordManager(hub)\n\n\t// Create test user and system\n\tuser, err := tests.CreateUser(hub, \"test@example.com\", \"testtesttest\")\n\trequire.NoError(t, err)\n\n\tsystem, err := tests.CreateRecord(hub, \"systems\", map[string]any{\n\t\t\"name\":   \"test-system\",\n\t\t\"host\":   \"localhost\",\n\t\t\"port\":   \"45876\",\n\t\t\"status\": \"up\",\n\t\t\"users\":  []string{user.Id},\n\t})\n\trequire.NoError(t, err)\n\n\tnow := time.Now().UTC()\n\n\t// Create old systemd service records that should be deleted (older than 20 minutes)\n\toldRecord, err := tests.CreateRecord(hub, \"systemd_services\", map[string]any{\n\t\t\"system\":  system.Id,\n\t\t\"name\":    \"nginx.service\",\n\t\t\"state\":   0, // Active\n\t\t\"sub\":     1, // Running\n\t\t\"cpu\":     5.0,\n\t\t\"cpuPeak\": 10.0,\n\t\t\"memory\":  1024000,\n\t\t\"memPeak\": 2048000,\n\t})\n\trequire.NoError(t, err)\n\t// Set updated time to 25 minutes ago (should be deleted)\n\toldRecord.SetRaw(\"updated\", now.Add(-25*time.Minute).UnixMilli())\n\terr = hub.SaveNoValidate(oldRecord)\n\trequire.NoError(t, err)\n\n\t// Create recent systemd service record that should be kept (within 20 minutes)\n\trecentRecord, err := tests.CreateRecord(hub, \"systemd_services\", map[string]any{\n\t\t\"system\":  system.Id,\n\t\t\"name\":    \"apache.service\",\n\t\t\"state\":   1, // Inactive\n\t\t\"sub\":     0, // Dead\n\t\t\"cpu\":     2.0,\n\t\t\"cpuPeak\": 3.0,\n\t\t\"memory\":  512000,\n\t\t\"memPeak\": 1024000,\n\t})\n\trequire.NoError(t, err)\n\t// Set updated time to 10 minutes ago (should be kept)\n\trecentRecord.SetRaw(\"updated\", now.Add(-10*time.Minute).UnixMilli())\n\terr = hub.SaveNoValidate(recentRecord)\n\trequire.NoError(t, err)\n\n\t// Count records before deletion\n\tcountBefore, err := hub.CountRecords(\"systemd_services\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(2), countBefore, \"Should have 2 systemd service records initially\")\n\n\t// Run deletion via RecordManager\n\trm.DeleteOldRecords()\n\n\t// Count records after deletion\n\tcountAfter, err := hub.CountRecords(\"systemd_services\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, int64(1), countAfter, \"Should have 1 systemd service record after deletion\")\n\n\t// Verify the correct record was kept\n\tremainingRecords, err := hub.FindRecordsByFilter(\"systemd_services\", \"\", \"\", 10, 0, nil)\n\trequire.NoError(t, err)\n\tassert.Len(t, remainingRecords, 1, \"Should have exactly 1 record remaining\")\n\tassert.Equal(t, \"apache.service\", remainingRecords[0].Get(\"name\"), \"The recent record should be kept\")\n}\n\n// TestRecordManagerCreation tests RecordManager creation\nfunc TestRecordManagerCreation(t *testing.T) {\n\thub, err := tests.NewTestHub(t.TempDir())\n\trequire.NoError(t, err)\n\tdefer hub.Cleanup()\n\n\trm := records.NewRecordManager(hub)\n\tassert.NotNil(t, rm, \"RecordManager should not be nil\")\n}\n\n// TestTwoDecimals tests the twoDecimals helper function\nfunc TestTwoDecimals(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput    float64\n\t\texpected float64\n\t}{\n\t\t{1.234567, 1.23},\n\t\t{1.235, 1.24}, // Should round up\n\t\t{1.0, 1.0},\n\t\t{0.0, 0.0},\n\t\t{-1.234567, -1.23},\n\t\t{-1.235, -1.23}, // Negative rounding\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := records.TwoDecimals(tc.input)\n\t\tassert.InDelta(t, tc.expected, result, 0.02, \"twoDecimals(%f) should equal %f\", tc.input, tc.expected)\n\t}\n}\n"
  },
  {
    "path": "internal/records/records_test_helpers.go",
    "content": "//go:build testing\n\npackage records\n\nimport (\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\n// DeleteOldSystemStats exposes deleteOldSystemStats for testing\nfunc DeleteOldSystemStats(app core.App) error {\n\treturn deleteOldSystemStats(app)\n}\n\n// DeleteOldAlertsHistory exposes deleteOldAlertsHistory for testing\nfunc DeleteOldAlertsHistory(app core.App, countToKeep, countBeforeDeletion int) error {\n\treturn deleteOldAlertsHistory(app, countToKeep, countBeforeDeletion)\n}\n\n// TwoDecimals exposes twoDecimals for testing\nfunc TwoDecimals(value float64) float64 {\n\treturn twoDecimals(value)\n}\n"
  },
  {
    "path": "internal/site/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "internal/site/.prettierrc",
    "content": "{\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}\n"
  },
  {
    "path": "internal/site/biome.json",
    "content": "{\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\"useIgnoreFile\": true,\n\t\t\"defaultBranch\": \"main\"\n\t},\n\t\"formatter\": {\n\t\t\"enabled\": true,\n\t\t\"indentStyle\": \"tab\",\n\t\t\"lineWidth\": 120,\n\t\t\"formatWithErrors\": true\n\t},\n\t\"assist\": { \"actions\": { \"source\": { \"organizeImports\": \"off\" } } },\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": true,\n\t\t\t\"a11y\": {\n\t\t\t\t\"useButtonType\": \"off\"\n\t\t\t},\n\t\t\t\"complexity\": {\n\t\t\t\t\"noUselessStringConcat\": \"error\",\n\t\t\t\t\"noUselessUndefinedInitialization\": \"error\",\n\t\t\t\t\"noVoid\": \"error\",\n\t\t\t\t\"useDateNow\": \"error\"\n\t\t\t},\n\t\t\t\"correctness\": {\n\t\t\t\t\"noConstantMathMinMaxClamp\": \"error\",\n\t\t\t\t\"noUndeclaredVariables\": \"error\",\n\t\t\t\t\"noUnusedImports\": \"error\",\n\t\t\t\t\"noUnusedFunctionParameters\": \"error\",\n\t\t\t\t\"noUnusedPrivateClassMembers\": \"error\",\n\t\t\t\t\"useExhaustiveDependencies\": {\n\t\t\t\t\t\"level\": \"off\"\n\t\t\t\t},\n\t\t\t\t\"useUniqueElementIds\": \"off\",\n\t\t\t\t\"noUnusedVariables\": \"error\"\n\t\t\t},\n\t\t\t\"security\": {\n\t\t\t\t\"noDangerouslySetInnerHtml\": \"warn\"\n\t\t\t},\n\t\t\t\"style\": {\n\t\t\t\t\"noParameterProperties\": \"error\",\n\t\t\t\t\"noYodaExpression\": \"error\",\n\t\t\t\t\"useConsistentBuiltinInstantiation\": \"error\",\n\t\t\t\t\"useFragmentSyntax\": \"error\",\n\t\t\t\t\"useShorthandAssign\": \"error\",\n\t\t\t\t\"useArrayLiterals\": \"error\"\n\t\t\t},\n\t\t\t\"suspicious\": {\n\t\t\t\t\"useAwait\": \"error\",\n\t\t\t\t\"noEvolvingTypes\": \"error\",\n\t\t\t\t\"noArrayIndexKey\": \"off\"\n\t\t\t}\n\t\t}\n\t},\n\t\"javascript\": {\n\t\t\"formatter\": {\n\t\t\t\"quoteStyle\": \"double\",\n\t\t\t\"trailingCommas\": \"es5\",\n\t\t\t\"semicolons\": \"asNeeded\"\n\t\t}\n\t},\n\t\"overrides\": [\n\t\t{\n\t\t\t\"includes\": [\"**/*.jsx\", \"**/*.tsx\"],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"style\": {\n\t\t\t\t\t\t\"noParameterAssign\": \"error\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"includes\": [\"**/*.ts\", \"**/*.tsx\"],\n\t\t\t\"linter\": {\n\t\t\t\t\"rules\": {\n\t\t\t\t\t\"correctness\": {\n\t\t\t\t\t\t\"noUnusedVariables\": \"off\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "internal/site/components.json",
    "content": "{\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\"config\": \"tailwind.config.js\",\n\t\t\"css\": \"src/index.css\",\n\t\t\"baseColor\": \"gray\",\n\t\t\"cssVariables\": true,\n\t\t\"prefix\": \"\"\n\t},\n\t\"aliases\": {\n\t\t\"components\": \"@/components\",\n\t\t\"utils\": \"@/lib/utils\"\n\t}\n}\n"
  },
  {
    "path": "internal/site/embed.go",
    "content": "// 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\nvar distDir embed.FS\n\n// DistDirFS contains the embedded dist directory files (without the \"dist\" prefix)\nvar DistDirFS, _ = fs.Sub(distDir, \"dist\")\n"
  },
  {
    "path": "internal/site/index.html",
    "content": "<!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/manifest.json\" crossorigin=\"use-credentials\" />\n\t\t<link rel=\"icon\" type=\"image/svg+xml\" href=\"./static/icon.svg\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no, viewport-fit=cover\" />\n\t\t<meta name=\"robots\" content=\"noindex, nofollow\" />\n\t\t<title>Beszel</title>\n\t\t<script>\n\t\t\tglobalThis.BESZEL = {\n\t\t\t\tBASE_PATH: \"%BASE_URL%\",\n\t\t\t\tHUB_VERSION: \"{{V}}\",\n\t\t\t\tHUB_URL: \"{{HUB_URL}}\"\n\t\t\t}\n\t\t</script>\n\t</head>\n\t<body>\n\t\t<div id=\"app\"></div>\n\t\t<script type=\"module\" src=\"/src/main.tsx\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "internal/site/lingui.config.ts",
    "content": "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\"da\",\n\t\t\"de\",\n\t\t\"es\",\n\t\t\"fa\",\n\t\t\"fr\",\n\t\t\"he\",\n\t\t\"hr\",\n\t\t\"hu\",\n\t\t\"id\",\n\t\t\"it\",\n\t\t\"ja\",\n\t\t\"ko\",\n\t\t\"nl\",\n\t\t\"no\",\n\t\t\"pl\",\n\t\t\"pt\",\n\t\t\"tr\",\n\t\t\"ru\",\n\t\t\"sl\",\n\t\t\"sr\",\n\t\t\"sv\",\n\t\t\"uk\",\n\t\t\"vi\",\n\t\t\"zh\",\n\t\t\"zh-CN\",\n\t\t\"zh-HK\",\n\t],\n\tsourceLocale: \"en\",\n\tcompileNamespace: \"ts\",\n\tformatOptions: {\n\t\tlineNumbers: false,\n\t},\n\tcatalogs: [\n\t\t{\n\t\t\tpath: \"<rootDir>/src/locales/{locale}/{locale}\",\n\t\t\tinclude: [\"src\"],\n\t\t},\n\t],\n})\n"
  },
  {
    "path": "internal/site/package.json",
    "content": "{\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\"build\": \"lingui extract --overwrite && lingui compile && vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"sync\": \"lingui extract --overwrite && lingui compile\",\n\t\t\"sync_no_compile\": \"lingui extract --overwrite --clean\",\n\t\t\"sync_and_purge\": \"lingui extract --overwrite --clean && lingui compile\",\n\t\t\"format\": \"biome format --write .\",\n\t\t\"lint\": \"biome lint .\",\n\t\t\"check\": \"biome check .\",\n\t\t\"check:fix\": \"biome check --fix .\"\n\t},\n\t\"dependencies\": {\n\t\t\"@henrygd/queue\": \"^1.0.7\",\n\t\t\"@henrygd/semaphore\": \"^0.0.2\",\n\t\t\"@lingui/detect-locale\": \"^5.4.1\",\n\t\t\"@lingui/macro\": \"^5.4.1\",\n\t\t\"@lingui/react\": \"^5.4.1\",\n\t\t\"@nanostores/react\": \"^0.7.3\",\n\t\t\"@nanostores/router\": \"^0.11.0\",\n\t\t\"@radix-ui/react-alert-dialog\": \"^1.1.15\",\n\t\t\"@radix-ui/react-checkbox\": \"^1.3.3\",\n\t\t\"@radix-ui/react-dialog\": \"^1.1.15\",\n\t\t\"@radix-ui/react-direction\": \"^1.1.1\",\n\t\t\"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n\t\t\"@radix-ui/react-label\": \"^2.1.7\",\n\t\t\"@radix-ui/react-select\": \"^2.2.6\",\n\t\t\"@radix-ui/react-separator\": \"^1.1.7\",\n\t\t\"@radix-ui/react-slider\": \"^1.3.6\",\n\t\t\"@radix-ui/react-slot\": \"^1.2.3\",\n\t\t\"@radix-ui/react-switch\": \"^1.2.6\",\n\t\t\"@radix-ui/react-tabs\": \"^1.1.13\",\n\t\t\"@radix-ui/react-toast\": \"^1.2.15\",\n\t\t\"@radix-ui/react-tooltip\": \"^1.2.8\",\n\t\t\"@tanstack/react-table\": \"^8.21.3\",\n\t\t\"@tanstack/react-virtual\": \"^3.13.12\",\n\t\t\"class-variance-authority\": \"^0.7.1\",\n\t\t\"clsx\": \"^2.1.1\",\n\t\t\"cmdk\": \"^1.1.1\",\n\t\t\"d3-time\": \"^3.1.0\",\n\t\t\"input-otp\": \"^1.4.2\",\n\t\t\"lucide-react\": \"^0.452.0\",\n\t\t\"nanostores\": \"^0.11.4\",\n\t\t\"pocketbase\": \"^0.26.2\",\n\t\t\"react\": \"^19.1.2\",\n\t\t\"react-dom\": \"^19.1.2\",\n\t\t\"recharts\": \"^2.15.4\",\n\t\t\"shiki\": \"^3.13.0\",\n\t\t\"tailwind-merge\": \"^3.3.1\",\n\t\t\"valibot\": \"^0.42.1\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@biomejs/biome\": \"2.2.4\",\n\t\t\"@lingui/cli\": \"^5.4.1\",\n\t\t\"@lingui/swc-plugin\": \"^5.6.1\",\n\t\t\"@lingui/vite-plugin\": \"^5.4.1\",\n\t\t\"@tailwindcss/container-queries\": \"^0.1.1\",\n\t\t\"@tailwindcss/vite\": \"^4.1.12\",\n\t\t\"@types/bun\": \"^1.2.20\",\n\t\t\"@types/react\": \"^19.1.11\",\n\t\t\"@types/react-dom\": \"^19.1.7\",\n\t\t\"@vitejs/plugin-react-swc\": \"^4.0.1\",\n\t\t\"tailwindcss\": \"^4.1.12\",\n\t\t\"tw-animate-css\": \"^1.3.7\",\n\t\t\"typescript\": \"^5.9.2\",\n\t\t\"vite\": \"^7.1.3\"\n\t},\n\t\"overrides\": {\n\t\t\"@nanostores/router\": {\n\t\t\t\"nanostores\": \"^0.11.3\"\n\t\t}\n\t},\n\t\"optionalDependencies\": {\n\t\t\"@esbuild/linux-arm64\": \"^0.21.5\"\n\t}\n}"
  },
  {
    "path": "internal/site/public/static/manifest.json",
    "content": "{\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  \"start_url\": \"../\",\n\t\"display\": \"standalone\",\n\t\"background_color\": \"#202225\",\n\t\"theme_color\": \"#202225\"\n}\n"
  },
  {
    "path": "internal/site/src/components/active-alerts.tsx",
    "content": "import { alertInfo } from \"@/lib/alerts\"\nimport { $alerts, $allSystemsById } from \"@/lib/stores\"\nimport type { AlertRecord } from \"@/types\"\nimport { Plural, Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { useMemo } from \"react\"\nimport { $router, Link } from \"./router\"\nimport { Alert, AlertTitle, AlertDescription } from \"./ui/alert\"\nimport { Card, CardHeader, CardTitle, CardContent } from \"./ui/card\"\n\nexport const ActiveAlerts = () => {\n\tconst alerts = useStore($alerts)\n\tconst systems = useStore($allSystemsById)\n\n\tconst { activeAlerts, alertsKey } = useMemo(() => {\n\t\tconst activeAlerts: AlertRecord[] = []\n\t\t// key to prevent re-rendering if alerts change but active alerts didn't\n\t\tconst alertsKey: string[] = []\n\n\t\tfor (const systemId of Object.keys(alerts)) {\n\t\t\tfor (const alert of alerts[systemId].values()) {\n\t\t\t\tif (alert.triggered && alert.name in alertInfo) {\n\t\t\t\t\tactiveAlerts.push(alert)\n\t\t\t\t\talertsKey.push(`${alert.system}${alert.value}${alert.min}`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { activeAlerts, alertsKey }\n\t}, [alerts])\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: alertsKey is inclusive\n\treturn useMemo(() => {\n\t\tif (activeAlerts.length === 0) {\n\t\t\treturn null\n\t\t}\n\t\treturn (\n\t\t\t<Card>\n\t\t\t\t<CardHeader className=\"pb-4 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1\">\n\t\t\t\t\t<div className=\"px-2 sm:px-1\">\n\t\t\t\t\t\t<CardTitle>\n\t\t\t\t\t\t\t<Trans>Active Alerts</Trans>\n\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t</div>\n\t\t\t\t</CardHeader>\n\t\t\t\t<CardContent className=\"max-sm:p-2\">\n\t\t\t\t\t{activeAlerts.length > 0 && (\n\t\t\t\t\t\t<div className=\"grid sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-3\">\n\t\t\t\t\t\t\t{activeAlerts.map((alert) => {\n\t\t\t\t\t\t\t\tconst info = alertInfo[alert.name as keyof typeof alertInfo]\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Alert\n\t\t\t\t\t\t\t\t\t\tkey={alert.id}\n\t\t\t\t\t\t\t\t\t\tclassName=\"hover:-translate-y-px duration-200 bg-transparent border-foreground/10 hover:shadow-md shadow-black/5\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<info.icon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t<AlertTitle>\n\t\t\t\t\t\t\t\t\t\t\t{systems[alert.system]?.name} {info.name()}\n\t\t\t\t\t\t\t\t\t\t</AlertTitle>\n\t\t\t\t\t\t\t\t\t\t<AlertDescription>\n\t\t\t\t\t\t\t\t\t\t\t{alert.name === \"Status\" ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Connection is down</Trans>\n\t\t\t\t\t\t\t\t\t\t\t) : info.invert ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\t\t\t\t\tBelow {alert.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{info.unit} in last <Plural value={alert.min} one=\"# minute\" other=\"# minutes\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\t\t\t\t\tExceeds {alert.value}\n\t\t\t\t\t\t\t\t\t\t\t\t\t{info.unit} in last <Plural value={alert.min} one=\"# minute\" other=\"# minutes\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</AlertDescription>\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\thref={getPagePath($router, \"system\", { id: systems[alert.system]?.id })}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"absolute inset-0 w-full h-full\"\n\t\t\t\t\t\t\t\t\t\t\taria-label=\"View system\"\n\t\t\t\t\t\t\t\t\t\t></Link>\n\t\t\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t)\n\t}, [alertsKey.join(\"\")])\n}\n"
  },
  {
    "path": "internal/site/src/components/add-system.tsx",
    "content": "import { msg, t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from \"lucide-react\"\nimport { memo, useEffect, useRef, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n\tDialog,\n\tDialogContent,\n\tDialogDescription,\n\tDialogFooter,\n\tDialogHeader,\n\tDialogTitle,\n\tDialogTrigger,\n} from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { isReadOnlyUser, pb } from \"@/lib/api\"\nimport { SystemStatus } from \"@/lib/enums\"\nimport { $publicKey } from \"@/lib/stores\"\nimport { cn, generateToken, tokenMap, useBrowserStorage } from \"@/lib/utils\"\nimport type { SystemRecord } from \"@/types\"\nimport {\n\tcopyDockerCompose,\n\tcopyDockerRun,\n\tcopyLinuxCommand,\n\tcopyWindowsCommand,\n\ttype DropdownItem,\n\tInstallDropdown,\n} from \"./install-dropdowns\"\nimport { $router, basePath, Link, navigate } from \"./router\"\nimport { DropdownMenu, DropdownMenuTrigger } from \"./ui/dropdown-menu\"\nimport { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from \"./ui/icons\"\nimport { InputCopy } from \"./ui/input-copy\"\n\nexport function AddSystemButton({ className }: { className?: string }) {\n\tif (isReadOnlyUser()) {\n\t\treturn null\n\t}\n\tconst [open, setOpen] = useState(false)\n\tconst opened = useRef(false)\n\tif (open) {\n\t\topened.current = true\n\t}\n\n\treturn (\n\t\t<Dialog open={open} onOpenChange={setOpen}>\n\t\t\t<DialogTrigger asChild>\n\t\t\t\t<Button variant=\"outline\" className={cn(\"flex gap-1 max-xs:h-[2.4rem]\", className)}>\n\t\t\t\t\t<PlusIcon className=\"h-4 w-4 450:-ms-1\" />\n\t\t\t\t\t<span className=\"hidden 450:inline\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tAdd <span className=\"hidden sm:inline\">System</span>\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</span>\n\t\t\t\t</Button>\n\t\t\t</DialogTrigger>\n\t\t\t{opened.current && <SystemDialog setOpen={setOpen} />}\n\t\t</Dialog>\n\t)\n}\n\n/**\n * Token to be used for the next system.\n * Prevents token changing if user copies config, then closes dialog and opens again.\n */\nlet nextSystemToken: string | null = null\n\n/**\n * SystemDialog component for adding or editing a system.\n * @param {Object} props - The component props.\n * @param {function} props.setOpen - Function to set the open state of the dialog.\n * @param {SystemRecord} [props.system] - Optional system record for editing an existing system.\n */\nexport const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) => void; system?: SystemRecord }) => {\n\tconst publicKey = useStore($publicKey)\n\tconst port = useRef<HTMLInputElement>(null)\n\tconst [hostValue, setHostValue] = useState(system?.host ?? \"\")\n\tconst isUnixSocket = hostValue.startsWith(\"/\")\n\tconst [tab, setTab] = useBrowserStorage(\"as-tab\", \"docker\")\n\tconst [token, setToken] = useState(system?.token ?? \"\")\n\n\tuseEffect(() => {\n\t\t;(async () => {\n\t\t\t// if no system, generate a new token\n\t\t\tif (!system) {\n\t\t\t\tnextSystemToken ||= generateToken()\n\t\t\t\treturn setToken(nextSystemToken)\n\t\t\t}\n\t\t\t// if system exists,get the token from the fingerprint record\n\t\t\tif (tokenMap.has(system.id)) {\n\t\t\t\treturn setToken(tokenMap.get(system.id)!)\n\t\t\t}\n\t\t\tconst { token } = await pb.collection(\"fingerprints\").getFirstListItem(`system = \"${system.id}\"`, {\n\t\t\t\tfields: \"token\",\n\t\t\t})\n\t\t\ttokenMap.set(system.id, token)\n\t\t\tsetToken(token)\n\t\t})()\n\t}, [system?.id, nextSystemToken])\n\n\tasync function handleSubmit(e: SubmitEvent) {\n\t\te.preventDefault()\n\t\tconst formData = new FormData(e.target as HTMLFormElement)\n\t\tconst data = Object.fromEntries(formData) as Record<string, any>\n\t\tdata.users = pb.authStore.record!.id\n\t\ttry {\n\t\t\tsetOpen(false)\n\t\t\tif (system) {\n\t\t\t\tawait pb.collection(\"systems\").update(system.id, { ...data, status: SystemStatus.Pending })\n\t\t\t} else {\n\t\t\t\tconst createdSystem = await pb.collection(\"systems\").create(data)\n\t\t\t\tawait pb.collection(\"fingerprints\").create({\n\t\t\t\t\tsystem: createdSystem.id,\n\t\t\t\t\ttoken,\n\t\t\t\t})\n\t\t\t\t// Reset the current token after successful system\n\t\t\t\t// creation so next system gets a new token\n\t\t\t\tnextSystemToken = null\n\t\t\t}\n\t\t\tnavigate(basePath)\n\t\t} catch (e) {\n\t\t\tconsole.error(e)\n\t\t}\n\t}\n\n\tconst systemTranslation = t`System`\n\n\treturn (\n\t\t<DialogContent\n\t\t\tclassName=\"w-[90%] sm:w-auto sm:ns-dialog max-w-full rounded-lg\"\n\t\t\tonCloseAutoFocus={() => {\n\t\t\t\tsetHostValue(system?.host ?? \"\")\n\t\t\t}}\n\t\t>\n\t\t\t<Tabs defaultValue={tab} onValueChange={setTab}>\n\t\t\t\t<DialogHeader>\n\t\t\t\t\t<DialogTitle className=\"mb-1 pb-1 max-w-100 truncate pr-8\">\n\t\t\t\t\t\t{system ? (\n\t\t\t\t\t\t\t<Trans>Edit {{ foo: systemTranslation }}</Trans>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<Trans>Add {{ foo: systemTranslation }}</Trans>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t<TabsList className=\"grid w-full grid-cols-2\">\n\t\t\t\t\t\t<TabsTrigger value=\"docker\">Docker</TabsTrigger>\n\t\t\t\t\t\t<TabsTrigger value=\"binary\">\n\t\t\t\t\t\t\t<Trans>Binary</Trans>\n\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t</TabsList>\n\t\t\t\t</DialogHeader>\n\t\t\t\t{/* Docker (set tab index to prevent auto focusing content in edit system dialog) */}\n\t\t\t\t<TabsContent value=\"docker\" tabIndex={-1}>\n\t\t\t\t\t<DialogDescription className=\"mb-3 leading-relaxed w-0 min-w-full\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tCopy the\n\t\t\t\t\t\t\t<code className=\"bg-muted px-1 rounded-sm leading-3\">docker-compose.yml</code> content for the agent\n\t\t\t\t\t\t\tbelow, or register agents automatically with a{\" \"}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\tonClick={() => setOpen(false)}\n\t\t\t\t\t\t\t\thref={getPagePath($router, \"settings\", { name: \"tokens\" })}\n\t\t\t\t\t\t\t\tclassName=\"link\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tuniversal token\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</DialogDescription>\n\t\t\t\t</TabsContent>\n\t\t\t\t{/* Binary */}\n\t\t\t\t<TabsContent value=\"binary\" tabIndex={-1}>\n\t\t\t\t\t<DialogDescription className=\"mb-3 leading-relaxed w-0 min-w-full\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tCopy the installation command for the agent below, or register agents automatically with a{\" \"}\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\tonClick={() => setOpen(false)}\n\t\t\t\t\t\t\t\thref={getPagePath($router, \"settings\", { name: \"tokens\" })}\n\t\t\t\t\t\t\t\tclassName=\"link\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tuniversal token\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</DialogDescription>\n\t\t\t\t</TabsContent>\n\t\t\t\t<form onSubmit={handleSubmit as any}>\n\t\t\t\t\t<div className=\"grid xs:grid-cols-[auto_1fr] gap-y-3 gap-x-4 items-center mt-1 mb-4\">\n\t\t\t\t\t\t<Label htmlFor=\"name\" className=\"xs:text-end\">\n\t\t\t\t\t\t\t<Trans>Name</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<Input id=\"name\" name=\"name\" defaultValue={system?.name} required />\n\t\t\t\t\t\t<Label htmlFor=\"host\" className=\"xs:text-end\">\n\t\t\t\t\t\t\t<Trans>Host / IP</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tid=\"host\"\n\t\t\t\t\t\t\tname=\"host\"\n\t\t\t\t\t\t\tvalue={hostValue}\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\tsetHostValue(e.target.value)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Label htmlFor=\"port\" className={cn(\"xs:text-end\", isUnixSocket && \"hidden\")}>\n\t\t\t\t\t\t\t<Trans>Port</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tref={port}\n\t\t\t\t\t\t\tname=\"port\"\n\t\t\t\t\t\t\tid=\"port\"\n\t\t\t\t\t\t\tdefaultValue={system?.port || \"45876\"}\n\t\t\t\t\t\t\trequired={!isUnixSocket}\n\t\t\t\t\t\t\tclassName={cn(isUnixSocket && \"hidden\")}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Label htmlFor=\"pkey\" className=\"xs:text-end whitespace-pre\">\n\t\t\t\t\t\t\t<Trans comment=\"Use 'Key' if your language requires many more characters\">Public Key</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<InputCopy value={publicKey} id=\"pkey\" name=\"pkey\" />\n\t\t\t\t\t\t<Label htmlFor=\"tkn\" className=\"xs:text-end whitespace-pre\">\n\t\t\t\t\t\t\t<Trans>Token</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<InputCopy value={token} id=\"tkn\" name=\"tkn\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<DialogFooter className=\"flex justify-end gap-x-2 gap-y-3 flex-col mt-5\">\n\t\t\t\t\t\t{/* Docker */}\n\t\t\t\t\t\t<TabsContent value=\"docker\" className=\"contents\">\n\t\t\t\t\t\t\t<CopyButton\n\t\t\t\t\t\t\t\ttext={t({ message: \"Copy docker compose\", context: \"Button to copy docker compose file content\" })}\n\t\t\t\t\t\t\t\tonClick={async () =>\n\t\t\t\t\t\t\t\t\tcopyDockerCompose(isUnixSocket ? hostValue : port.current?.value, publicKey, token)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\ticon={<DockerIcon className=\"size-4 -me-0.5\" />}\n\t\t\t\t\t\t\t\tdropdownItems={[\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttext: t({ message: \"Copy docker run\", context: \"Button to copy docker run command\" }),\n\t\t\t\t\t\t\t\t\t\tonClick: async () =>\n\t\t\t\t\t\t\t\t\t\t\tcopyDockerRun(isUnixSocket ? hostValue : port.current?.value, publicKey, token),\n\t\t\t\t\t\t\t\t\t\ticons: [DockerIcon],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t\t{/* Binary */}\n\t\t\t\t\t\t<TabsContent value=\"binary\" className=\"contents\">\n\t\t\t\t\t\t\t<CopyButton\n\t\t\t\t\t\t\t\ttext={t`Copy Linux command`}\n\t\t\t\t\t\t\t\ticon={<TuxIcon className=\"size-4\" />}\n\t\t\t\t\t\t\t\tonClick={async () => copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token)}\n\t\t\t\t\t\t\t\tdropdownItems={[\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttext: t({ message: \"Homebrew command\", context: \"Button to copy install command\" }),\n\t\t\t\t\t\t\t\t\t\tonClick: async () =>\n\t\t\t\t\t\t\t\t\t\t\tcopyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token, true),\n\t\t\t\t\t\t\t\t\t\ticons: [AppleIcon, TuxIcon],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttext: t({ message: \"Windows command\", context: \"Button to copy install command\" }),\n\t\t\t\t\t\t\t\t\t\tonClick: async () =>\n\t\t\t\t\t\t\t\t\t\t\tcopyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),\n\t\t\t\t\t\t\t\t\t\ticons: [WindowsIcon],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttext: t({ message: \"FreeBSD command\", context: \"Button to copy install command\" }),\n\t\t\t\t\t\t\t\t\t\tonClick: async () =>\n\t\t\t\t\t\t\t\t\t\t\tcopyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token),\n\t\t\t\t\t\t\t\t\t\ticons: [FreeBsdIcon],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttext: t`Manual setup instructions`,\n\t\t\t\t\t\t\t\t\t\turl: \"https://beszel.dev/guide/agent-installation#binary\",\n\t\t\t\t\t\t\t\t\t\ticons: [ExternalLinkIcon],\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t\t{/* Save */}\n\t\t\t\t\t\t<Button>{system ? <Trans>Save system</Trans> : <Trans>Add system</Trans>}</Button>\n\t\t\t\t\t</DialogFooter>\n\t\t\t\t</form>\n\t\t\t</Tabs>\n\t\t</DialogContent>\n\t)\n}\n\ninterface CopyButtonProps {\n\ttext: string\n\tonClick: () => void\n\tdropdownItems: DropdownItem[]\n\ticon?: React.ReactElement<any>\n}\n\nconst CopyButton = memo((props: CopyButtonProps) => {\n\treturn (\n\t\t<div className=\"flex gap-0 rounded-lg\">\n\t\t\t<Button\n\t\t\t\ttype=\"button\"\n\t\t\t\tvariant=\"outline\"\n\t\t\t\tonClick={props.onClick}\n\t\t\t\tclassName=\"rounded-e-none dark:border-e-0 grow flex items-center gap-2\"\n\t\t\t>\n\t\t\t\t{props.text} {props.icon}\n\t\t\t</Button>\n\t\t\t<div className=\"w-px h-full bg-muted\"></div>\n\t\t\t<DropdownMenu>\n\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t<Button variant=\"outline\" className={\"px-2 rounded-s-none border-s-0\"}>\n\t\t\t\t\t\t<ChevronDownIcon />\n\t\t\t\t\t</Button>\n\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t<InstallDropdown items={props.dropdownItems} />\n\t\t\t</DropdownMenu>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/alerts/alert-button.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { BellIcon } from \"lucide-react\"\nimport { memo, useMemo, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Sheet, SheetContent, SheetTrigger } from \"@/components/ui/sheet\"\nimport { $alerts } from \"@/lib/stores\"\nimport { cn } from \"@/lib/utils\"\nimport type { SystemRecord } from \"@/types\"\nimport { AlertDialogContent } from \"./alerts-sheet\"\n\nexport default memo(function AlertsButton({ system }: { system: SystemRecord }) {\n\tconst [opened, setOpened] = useState(false)\n\tconst alerts = useStore($alerts)\n\n\tconst hasSystemAlert = alerts[system.id]?.size > 0\n\treturn useMemo(\n\t\t() => (\n\t\t\t<Sheet>\n\t\t\t\t<SheetTrigger asChild>\n\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" aria-label={t`Alerts`} data-nolink onClick={() => setOpened(true)}>\n\t\t\t\t\t\t<BellIcon\n\t\t\t\t\t\t\tclassName={cn(\"h-[1.2em] w-[1.2em] pointer-events-none\", {\n\t\t\t\t\t\t\t\t\"fill-primary\": hasSystemAlert,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Button>\n\t\t\t\t</SheetTrigger>\n\t\t\t\t<SheetContent className=\"max-h-full overflow-auto w-160 !max-w-full p-4 sm:p-6\">\n\t\t\t\t\t{opened && <AlertDialogContent system={system} />}\n\t\t\t\t</SheetContent>\n\t\t\t</Sheet>\n\t\t),\n\t\t[opened, hasSystemAlert]\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/alerts/alerts-sheet.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Plural, Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { GlobeIcon, ServerIcon } from \"lucide-react\"\nimport { lazy, memo, Suspense, useMemo, useState } from \"react\"\nimport { $router, Link } from \"@/components/router\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport { alertInfo } from \"@/lib/alerts\"\nimport { pb } from \"@/lib/api\"\nimport { $alerts, $systems } from \"@/lib/stores\"\nimport { cn, debounce } from \"@/lib/utils\"\nimport type { AlertInfo, AlertRecord, SystemRecord } from \"@/types\"\n\nconst Slider = lazy(() => import(\"@/components/ui/slider\"))\n\nconst endpoint = \"/api/beszel/user-alerts\"\n\nconst alertDebounce = 400\n\nconst alertKeys = Object.keys(alertInfo) as (keyof typeof alertInfo)[]\n\nconst failedUpdateToast = (error: unknown) => {\n\tconsole.error(error)\n\ttoast({\n\t\ttitle: t`Failed to update alert`,\n\t\tdescription: t`Please check logs for more details.`,\n\t\tvariant: \"destructive\",\n\t})\n}\n\n/** Create or update alerts for a given name and systems */\nconst upsertAlerts = debounce(\n\tasync ({ name, value, min, systems }: { name: string; value: number; min: number; systems: string[] }) => {\n\t\ttry {\n\t\t\tawait pb.send<{ success: boolean }>(endpoint, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\t// overwrite is always true because we've done filtering client side\n\t\t\t\tbody: { name, value, min, systems, overwrite: true },\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tfailedUpdateToast(error)\n\t\t}\n\t},\n\talertDebounce\n)\n\n/** Delete alerts for a given name and systems */\nconst deleteAlerts = debounce(async ({ name, systems }: { name: string; systems: string[] }) => {\n\ttry {\n\t\tawait pb.send<{ success: boolean }>(endpoint, {\n\t\t\tmethod: \"DELETE\",\n\t\t\tbody: { name, systems },\n\t\t})\n\t} catch (error) {\n\t\tfailedUpdateToast(error)\n\t}\n}, alertDebounce)\n\nexport const AlertDialogContent = memo(function AlertDialogContent({ system }: { system: SystemRecord }) {\n\tconst alerts = useStore($alerts)\n\tconst [overwriteExisting, setOverwriteExisting] = useState<boolean | \"indeterminate\">(false)\n\tconst [currentTab, setCurrentTab] = useState(\"system\")\n\n\tconst systemAlerts = alerts[system.id] ?? new Map()\n\n\t// We need to keep a copy of alerts when we switch to global tab. If we always compare to\n\t// current alerts, it will only be updated when first checked, then won't be updated because\n\t// after that it exists.\n\tconst alertsWhenGlobalSelected = useMemo(() => {\n\t\treturn currentTab === \"global\" ? structuredClone(alerts) : alerts\n\t}, [currentTab])\n\n\treturn (\n\t\t<>\n\t\t\t<DialogHeader>\n\t\t\t\t<DialogTitle className=\"text-xl\">\n\t\t\t\t\t<Trans>Alerts</Trans>\n\t\t\t\t</DialogTitle>\n\t\t\t\t<DialogDescription>\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\tSee{\" \"}\n\t\t\t\t\t\t<Link href={getPagePath($router, \"settings\", { name: \"notifications\" })} className=\"link\">\n\t\t\t\t\t\t\tnotification settings\n\t\t\t\t\t\t</Link>{\" \"}\n\t\t\t\t\t\tto configure how you receive alerts.\n\t\t\t\t\t</Trans>\n\t\t\t\t</DialogDescription>\n\t\t\t</DialogHeader>\n\t\t\t<Tabs defaultValue=\"system\" onValueChange={setCurrentTab}>\n\t\t\t\t<TabsList className=\"mb-1 -mt-0.5\">\n\t\t\t\t\t<TabsTrigger value=\"system\">\n\t\t\t\t\t\t<ServerIcon className=\"me-2 h-3.5 w-3.5\" />\n\t\t\t\t\t\t<span className=\"truncate max-w-60\">{system.name}</span>\n\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t<TabsTrigger value=\"global\">\n\t\t\t\t\t\t<GlobeIcon className=\"me-1.5 h-3.5 w-3.5\" />\n\t\t\t\t\t\t<Trans>All Systems</Trans>\n\t\t\t\t\t</TabsTrigger>\n\t\t\t\t</TabsList>\n\t\t\t\t<TabsContent value=\"system\">\n\t\t\t\t\t<div className=\"grid gap-3\">\n\t\t\t\t\t\t{alertKeys.map((name) => (\n\t\t\t\t\t\t\t<AlertContent\n\t\t\t\t\t\t\t\tkey={name}\n\t\t\t\t\t\t\t\talertKey={name}\n\t\t\t\t\t\t\t\tdata={alertInfo[name as keyof typeof alertInfo]}\n\t\t\t\t\t\t\t\talert={systemAlerts.get(name)}\n\t\t\t\t\t\t\t\tsystem={system}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</TabsContent>\n\t\t\t\t<TabsContent value=\"global\">\n\t\t\t\t\t<label\n\t\t\t\t\t\thtmlFor=\"ovw\"\n\t\t\t\t\t\tclassName=\"mb-3 flex gap-2 items-center justify-center cursor-pointer border rounded-sm py-3 px-4 border-destructive text-destructive font-semibold text-sm\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\tid=\"ovw\"\n\t\t\t\t\t\t\tclassName=\"text-destructive border-destructive data-[state=checked]:bg-destructive\"\n\t\t\t\t\t\t\tchecked={overwriteExisting}\n\t\t\t\t\t\t\tonCheckedChange={setOverwriteExisting}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<Trans>Overwrite existing alerts</Trans>\n\t\t\t\t\t</label>\n\t\t\t\t\t<div className=\"grid gap-3\">\n\t\t\t\t\t\t{alertKeys.map((name) => (\n\t\t\t\t\t\t\t<AlertContent\n\t\t\t\t\t\t\t\tkey={name}\n\t\t\t\t\t\t\t\talertKey={name}\n\t\t\t\t\t\t\t\tsystem={system}\n\t\t\t\t\t\t\t\talert={systemAlerts.get(name)}\n\t\t\t\t\t\t\t\tdata={alertInfo[name as keyof typeof alertInfo]}\n\t\t\t\t\t\t\t\tglobal={true}\n\t\t\t\t\t\t\t\toverwriteExisting={!!overwriteExisting}\n\t\t\t\t\t\t\t\tinitialAlertsState={alertsWhenGlobalSelected}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</TabsContent>\n\t\t\t</Tabs>\n\t\t</>\n\t)\n})\n\nexport function AlertContent({\n\talertKey,\n\tdata: alertData,\n\tsystem,\n\talert,\n\tglobal = false,\n\toverwriteExisting = false,\n\tinitialAlertsState = {},\n}: {\n\talertKey: string\n\tdata: AlertInfo\n\tsystem: SystemRecord\n\talert?: AlertRecord\n\tglobal?: boolean\n\toverwriteExisting?: boolean\n\tinitialAlertsState?: Record<string, Map<string, AlertRecord>>\n}) {\n\tconst { name } = alertData\n\n\tconst singleDescription = alertData.singleDesc?.()\n\n\tconst [checked, setChecked] = useState(global ? false : !!alert)\n\tconst [min, setMin] = useState(alert?.min || 10)\n\tconst [value, setValue] = useState(alert?.value || (singleDescription ? 0 : (alertData.start ?? 80)))\n\n\tconst Icon = alertData.icon\n\n\t/** Get system ids to update */\n\tfunction getSystemIds(): string[] {\n\t\t// if not global, update only the current system\n\t\tif (!global) {\n\t\t\treturn [system.id]\n\t\t}\n\t\t// if global, update all systems when overwriteExisting is true\n\t\t// update only systems without an existing alert when overwriteExisting is false\n\t\tconst allSystems = $systems.get()\n\t\tconst systemIds: string[] = []\n\t\tfor (const system of allSystems) {\n\t\t\tif (overwriteExisting || !initialAlertsState[system.id]?.has(alertKey)) {\n\t\t\t\tsystemIds.push(system.id)\n\t\t\t}\n\t\t}\n\t\treturn systemIds\n\t}\n\n\tfunction sendUpsert(min: number, value: number) {\n\t\tconst systems = getSystemIds()\n\t\tsystems.length &&\n\t\t\tupsertAlerts({\n\t\t\t\tname: alertKey,\n\t\t\t\tvalue,\n\t\t\t\tmin,\n\t\t\t\tsystems,\n\t\t\t})\n\t}\n\n\treturn (\n\t\t<div className=\"rounded-lg border border-muted-foreground/15 hover:border-muted-foreground/20 transition-colors duration-100 group\">\n\t\t\t<label\n\t\t\t\thtmlFor={`s${name}`}\n\t\t\t\tclassName={cn(\"flex flex-row items-center justify-between gap-4 cursor-pointer p-4\", {\n\t\t\t\t\t\"pb-0\": checked,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<div className=\"grid gap-1 select-none\">\n\t\t\t\t\t<p className=\"font-semibold flex gap-3 items-center\">\n\t\t\t\t\t\t<Icon className=\"h-4 w-4 opacity-85\" /> {alertData.name()}\n\t\t\t\t\t</p>\n\t\t\t\t\t{!checked && <span className=\"block text-sm text-muted-foreground\">{alertData.desc()}</span>}\n\t\t\t\t</div>\n\t\t\t\t<Switch\n\t\t\t\t\tid={`s${name}`}\n\t\t\t\t\tchecked={checked}\n\t\t\t\t\tonCheckedChange={(newChecked) => {\n\t\t\t\t\t\tsetChecked(newChecked)\n\t\t\t\t\t\tif (newChecked) {\n\t\t\t\t\t\t\t// if alert checked, create or update alert\n\t\t\t\t\t\t\tsendUpsert(min, value)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// if unchecked, delete alert (unless global and overwriteExisting is false)\n\t\t\t\t\t\t\tdeleteAlerts({ name: alertKey, systems: getSystemIds() })\n\t\t\t\t\t\t\t// when force deleting all alerts of a type, also remove them from initialAlertsState\n\t\t\t\t\t\t\tif (overwriteExisting) {\n\t\t\t\t\t\t\t\tfor (const curAlerts of Object.values(initialAlertsState)) {\n\t\t\t\t\t\t\t\t\tcurAlerts.delete(alertKey)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</label>\n\t\t\t{checked && (\n\t\t\t\t<div className=\"grid sm:grid-cols-2 mt-1.5 gap-5 px-4 pb-5 tabular-nums text-muted-foreground\">\n\t\t\t\t\t<Suspense fallback={<div className=\"h-10\" />}>\n\t\t\t\t\t\t{!singleDescription && (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<p id={`v${name}`} className=\"text-sm block h-6\">\n\t\t\t\t\t\t\t\t\t{alertData.invert ? (\n\t\t\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\t\t\tAverage drops below{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t<strong className=\"text-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\t\t\t\t{alertData.unit}\n\t\t\t\t\t\t\t\t\t\t\t</strong>\n\t\t\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\t\t\tAverage exceeds{\" \"}\n\t\t\t\t\t\t\t\t\t\t\t<strong className=\"text-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\t\t\t\t{alertData.unit}\n\t\t\t\t\t\t\t\t\t\t\t</strong>\n\t\t\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t<div className=\"flex gap-3 items-center\">\n\t\t\t\t\t\t\t\t\t<Slider\n\t\t\t\t\t\t\t\t\t\taria-labelledby={`v${name}`}\n\t\t\t\t\t\t\t\t\t\tvalue={[value]}\n\t\t\t\t\t\t\t\t\t\tonValueCommit={(val) => sendUpsert(min, val[0])}\n\t\t\t\t\t\t\t\t\t\tonValueChange={(val) => setValue(val[0])}\n\t\t\t\t\t\t\t\t\t\tstep={alertData.step ?? 1}\n\t\t\t\t\t\t\t\t\t\tmin={alertData.min ?? 1}\n\t\t\t\t\t\t\t\t\t\tmax={alertData.max ?? 99}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\t\tvalue={value}\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\t\t\t\tlet val = parseFloat(e.target.value)\n\t\t\t\t\t\t\t\t\t\t\tif (!Number.isNaN(val)) {\n\t\t\t\t\t\t\t\t\t\t\t\tif (alertData.max != null) val = Math.min(val, alertData.max)\n\t\t\t\t\t\t\t\t\t\t\t\tif (alertData.min != null) val = Math.max(val, alertData.min)\n\t\t\t\t\t\t\t\t\t\t\t\tsetValue(val)\n\t\t\t\t\t\t\t\t\t\t\t\tsendUpsert(min, val)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tstep={alertData.step ?? 1}\n\t\t\t\t\t\t\t\t\t\tmin={alertData.min ?? 1}\n\t\t\t\t\t\t\t\t\t\tmax={alertData.max ?? 99}\n\t\t\t\t\t\t\t\t\t\tclassName=\"w-16 h-8 text-center px-1\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div className={cn(singleDescription && \"col-span-full lowercase\")}>\n\t\t\t\t\t\t\t<p id={`t${name}`} className=\"text-sm block h-6 first-letter:uppercase\">\n\t\t\t\t\t\t\t\t{singleDescription && (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t{singleDescription}\n\t\t\t\t\t\t\t\t\t\t{` `}\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tFor <strong className=\"text-foreground\">{min}</strong>{\" \"}\n\t\t\t\t\t\t\t\t\t<Plural value={min} one=\"minute\" other=\"minutes\" />\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<div className=\"flex gap-3 items-center\">\n\t\t\t\t\t\t\t\t<Slider\n\t\t\t\t\t\t\t\t\taria-labelledby={`t${name}`}\n\t\t\t\t\t\t\t\t\tvalue={[min]}\n\t\t\t\t\t\t\t\t\tonValueCommit={(val) => sendUpsert(val[0], value)}\n\t\t\t\t\t\t\t\t\tonValueChange={(val) => setMin(val[0])}\n\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\tmax={60}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\t\tvalue={min}\n\t\t\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\t\t\tlet val = parseInt(e.target.value, 10)\n\t\t\t\t\t\t\t\t\t\tif (!Number.isNaN(val)) {\n\t\t\t\t\t\t\t\t\t\t\tval = Math.max(1, Math.min(val, 60))\n\t\t\t\t\t\t\t\t\t\t\tsetMin(val)\n\t\t\t\t\t\t\t\t\t\t\tsendUpsert(val, value)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\t\tmax={60}\n\t\t\t\t\t\t\t\t\tclassName=\"w-16 h-8 text-center px-1\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Suspense>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/alerts-history-columns.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport type { ColumnDef } from \"@tanstack/react-table\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { alertInfo } from \"@/lib/alerts\"\nimport { cn, formatDuration, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { AlertsHistoryRecord } from \"@/types\"\n\nexport const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [\n\t{\n\t\taccessorKey: \"system\",\n\t\tenableSorting: true,\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans>System</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ row }) => (\n\t\t\t<div className=\"ps-2 max-w-60 truncate\">{row.original.expand?.system?.name || row.original.system}</div>\n\t\t),\n\t\tfilterFn: (row, _, filterValue) => {\n\t\t\tconst display = row.original.expand?.system?.name || row.original.system || \"\"\n\t\t\treturn display.toLowerCase().includes(filterValue.toLowerCase())\n\t\t},\n\t},\n\t{\n\t\t// accessorKey: \"name\",\n\t\tid: \"name\",\n\t\taccessorFn: (record) => {\n\t\t\tconst name = record.name\n\t\t\tconst info = alertInfo[name]\n\t\t\treturn info?.name().replace(\"cpu\", \"CPU\") || name\n\t\t},\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans>Name</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ getValue, row }) => {\n\t\t\tconst name = getValue() as string\n\t\t\tconst info = alertInfo[row.original.name]\n\t\t\tconst Icon = info?.icon\n\n\t\t\treturn (\n\t\t\t\t<span className=\"flex items-center gap-2 ps-1 min-w-40\">\n\t\t\t\t\t{Icon && <Icon className=\"size-3.5\" />}\n\t\t\t\t\t{name}\n\t\t\t\t</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"value\",\n\t\tenableSorting: false,\n\t\theader: () => (\n\t\t\t<Button variant=\"ghost\">\n\t\t\t\t<Trans>Value</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell({ row, getValue }) {\n\t\t\tconst name = row.original.name\n\t\t\tif (name === \"Status\") {\n\t\t\t\treturn <span className=\"ps-2\">{t`Down`}</span>\n\t\t\t}\n\t\t\tconst value = getValue() as number\n\t\t\tconst unit = alertInfo[name]?.unit\n\t\t\treturn (\n\t\t\t\t<span className=\"tabular-nums ps-2.5\">\n\t\t\t\t\t{toFixedFloat(value, value < 10 ? 2 : 1)}\n\t\t\t\t\t{unit}\n\t\t\t\t</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"state\",\n\t\tenableSorting: true,\n\t\tsortingFn: (rowA, rowB) => (rowA.original.resolved ? 1 : 0) - (rowB.original.resolved ? 1 : 0),\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans comment=\"Context: alert state (active or resolved)\">State</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ row }) => {\n\t\t\tconst resolved = row.original.resolved\n\t\t\treturn (\n\t\t\t\t<Badge\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"capitalize pointer-events-none\",\n\t\t\t\t\t\tresolved\n\t\t\t\t\t\t\t? \"bg-green-100 text-green-800 border-green-200 dark:opacity-80\"\n\t\t\t\t\t\t\t: \"bg-yellow-100 text-yellow-800 border-yellow-200\"\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{/* {resolved ? <CircleCheckIcon className=\"size-3 me-0.5\" /> : <CircleAlertIcon className=\"size-3 me-0.5\" />} */}\n\t\t\t\t\t{resolved ? <Trans>Resolved</Trans> : <Trans>Active</Trans>}\n\t\t\t\t</Badge>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"created\",\n\t\taccessorFn: (record) => formatShortDate(record.created),\n\t\tenableSorting: true,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans comment=\"Context: date created\">Created</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ getValue, row }) => (\n\t\t\t<span className=\"ps-1 tabular-nums tracking-tight\" title={`${row.original.created} UTC`}>\n\t\t\t\t{getValue() as string}\n\t\t\t</span>\n\t\t),\n\t},\n\t{\n\t\taccessorKey: \"resolved\",\n\t\tenableSorting: true,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans>Resolved</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ row, getValue }) => {\n\t\t\tconst resolved = getValue() as string | null\n\t\t\tif (!resolved) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn (\n\t\t\t\t<span className=\"ps-1 tabular-nums tracking-tight\" title={`${row.original.resolved} UTC`}>\n\t\t\t\t\t{formatShortDate(resolved)}\n\t\t\t\t</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"duration\",\n\t\tinvertSorting: true,\n\t\tenableSorting: true,\n\t\tsortingFn: (rowA, rowB) => {\n\t\t\tconst aCreated = new Date(rowA.original.created)\n\t\t\tconst bCreated = new Date(rowB.original.created)\n\t\t\tconst aResolved = rowA.original.resolved ? new Date(rowA.original.resolved) : null\n\t\t\tconst bResolved = rowB.original.resolved ? new Date(rowB.original.resolved) : null\n\t\t\tconst aDuration = aResolved ? aResolved.getTime() - aCreated.getTime() : null\n\t\t\tconst bDuration = bResolved ? bResolved.getTime() - bCreated.getTime() : null\n\t\t\tif (!aDuration && bDuration) return -1\n\t\t\tif (aDuration && !bDuration) return 1\n\t\t\treturn (aDuration || 0) - (bDuration || 0)\n\t\t},\n\t\theader: ({ column }) => (\n\t\t\t<Button variant=\"ghost\" onClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}>\n\t\t\t\t<Trans>Duration</Trans>\n\t\t\t</Button>\n\t\t),\n\t\tcell: ({ row }) => {\n\t\t\tconst duration = formatDuration(row.original.created, row.original.resolved)\n\t\t\tif (!duration) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn <span className=\"ps-2\">{duration}</span>\n\t\t},\n\t},\n]\n"
  },
  {
    "path": "internal/site/src/components/charts/area-chart.tsx",
    "content": "import { useMemo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport {\n\tChartContainer,\n\tChartLegend,\n\tChartLegendContent,\n\tChartTooltip,\n\tChartTooltipContent,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { chartMargin, cn, formatShortDate } from \"@/lib/utils\"\nimport type { ChartData, SystemStatsRecord } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\nimport { AxisDomain } from \"recharts/types/util/types\"\n\nexport type DataPoint = {\n\tlabel: string\n\tdataKey: (data: SystemStatsRecord) => number | undefined\n\tcolor: number | string\n\topacity: number\n\tstackId?: string | number\n}\n\nexport default function AreaChartDefault({\n\tchartData,\n\tmax,\n\tmaxToggled,\n\ttickFormatter,\n\tcontentFormatter,\n\tdataPoints,\n\tdomain,\n\tlegend,\n\titemSorter,\n\tshowTotal = false,\n\treverseStackOrder = false,\n\thideYAxis = false,\n}: // logRender = false,\n\t{\n\t\tchartData: ChartData\n\t\tmax?: number\n\t\tmaxToggled?: boolean\n\t\ttickFormatter: (value: number, index: number) => string\n\t\tcontentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string\n\t\tdataPoints?: DataPoint[]\n\t\tdomain?: AxisDomain\n\t\tlegend?: boolean\n\t\tshowTotal?: boolean\n\t\titemSorter?: (a: any, b: any) => number\n\t\treverseStackOrder?: boolean\n\t\thideYAxis?: boolean\n\t\t// logRender?: boolean\n\t}) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: ignore\n\treturn useMemo(() => {\n\t\tif (chartData.systemStats.length === 0) {\n\t\t\treturn null\n\t\t}\n\t\t// if (logRender) {\n\t\t// \tconsole.log(\"Rendered at\", new Date())\n\t\t// }\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<ChartContainer\n\t\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\t\"opacity-100\": yAxisWidth || hideYAxis,\n\t\t\t\t\t\t\"ps-4\": hideYAxis,\n\t\t\t\t\t})}\n\t\t\t\t>\n\t\t\t\t\t<AreaChart\n\t\t\t\t\t\treverseStackOrder={reverseStackOrder}\n\t\t\t\t\t\taccessibilityLayer\n\t\t\t\t\t\tdata={chartData.systemStats}\n\t\t\t\t\t\tmargin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}\n\t\t\t\t\t>\n\t\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t\t{!hideYAxis && (\n\t\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\t\t\tdomain={domain ?? [0, max ?? \"auto\"]}\n\t\t\t\t\t\t\t\ttickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}\n\t\t\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\t\titemSorter={itemSorter}\n\t\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\t\tcontentFormatter={contentFormatter}\n\t\t\t\t\t\t\t\t\tshowTotal={showTotal}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{dataPoints?.map((dataPoint) => {\n\t\t\t\t\t\t\tlet { color } = dataPoint\n\t\t\t\t\t\t\tif (typeof color === \"number\") {\n\t\t\t\t\t\t\t\tcolor = `var(--chart-${color})`\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Area\n\t\t\t\t\t\t\t\t\tkey={dataPoint.label}\n\t\t\t\t\t\t\t\t\tdataKey={dataPoint.dataKey}\n\t\t\t\t\t\t\t\t\tname={dataPoint.label}\n\t\t\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\t\t\tfill={color}\n\t\t\t\t\t\t\t\t\tfillOpacity={dataPoint.opacity}\n\t\t\t\t\t\t\t\t\tstroke={color}\n\t\t\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t\t\t\tstackId={dataPoint.stackId}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{legend && <ChartLegend content={<ChartLegendContent reverse={reverseStackOrder} />} />}\n\t\t\t\t\t</AreaChart>\n\t\t\t\t</ChartContainer>\n\t\t\t</div>\n\t\t)\n\t}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal])\n}\n"
  },
  {
    "path": "internal/site/src/components/charts/chart-time-select.tsx",
    "content": "import { useStore } from \"@nanostores/react\"\nimport { HistoryIcon } from \"lucide-react\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { $chartTime } from \"@/lib/stores\"\nimport { chartTimeData, cn, compareSemVer, parseSemVer } from \"@/lib/utils\"\nimport type { ChartTimes, SemVer } from \"@/types\"\nimport { memo } from \"react\"\n\nexport default memo(function ChartTimeSelect({\n\tclassName,\n\tagentVersion,\n}: {\n\tclassName?: string\n\tagentVersion: SemVer\n}) {\n\tconst chartTime = useStore($chartTime)\n\n\t// remove chart times that are not supported by the system agent version\n\tconst availableChartTimes = Object.entries(chartTimeData).filter(([_, { minVersion }]) => {\n\t\tif (!minVersion) {\n\t\t\treturn true\n\t\t}\n\t\treturn compareSemVer(agentVersion, parseSemVer(minVersion)) >= 0\n\t})\n\n\treturn (\n\t\t<Select defaultValue=\"1h\" value={chartTime} onValueChange={(value: ChartTimes) => $chartTime.set(value)}>\n\t\t\t<SelectTrigger className={cn(className, \"relative ps-10 pe-5\")}>\n\t\t\t\t<HistoryIcon className=\"h-4 w-4 absolute start-4 top-1/2 -translate-y-1/2 opacity-85\" />\n\t\t\t\t<SelectValue />\n\t\t\t</SelectTrigger>\n\t\t\t<SelectContent>\n\t\t\t\t{availableChartTimes.map(([value, { label }]) => (\n\t\t\t\t\t<SelectItem key={value} value={value}>\n\t\t\t\t\t\t{label()}\n\t\t\t\t\t</SelectItem>\n\t\t\t\t))}\n\t\t\t</SelectContent>\n\t\t</Select>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/container-chart.tsx",
    "content": "// import Spinner from '../spinner'\nimport { useStore } from \"@nanostores/react\"\nimport { memo, useMemo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport {\n\ttype ChartConfig,\n\tChartContainer,\n\tChartTooltip,\n\tChartTooltipContent,\n\tpinnedAxisDomain,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { ChartType, Unit } from \"@/lib/enums\"\nimport { $containerFilter, $userSettings } from \"@/lib/stores\"\nimport { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { Separator } from \"../ui/separator\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function ContainerChart({\n\tdataKey,\n\tchartData,\n\tchartType,\n\tchartConfig,\n\tunit = \"%\",\n}: {\n\tdataKey: string\n\tchartData: ChartData\n\tchartType: ChartType\n\tchartConfig: ChartConfig\n\tunit?: string\n}) {\n\tconst filter = useStore($containerFilter)\n\tconst userSettings = useStore($userSettings)\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\n\tconst { containerData } = chartData\n\n\tconst isNetChart = chartType === ChartType.Network\n\n\t// Filter with set lookup\n\tconst filteredKeys = useMemo(() => {\n\t\tif (!filter) {\n\t\t\treturn new Set<string>()\n\t\t}\n\t\tconst filterTerms = filter\n\t\t\t.toLowerCase()\n\t\t\t.split(\" \")\n\t\t\t.filter((term) => term.length > 0)\n\t\treturn new Set(\n\t\t\tObject.keys(chartConfig).filter((key) => {\n\t\t\t\tconst keyLower = key.toLowerCase()\n\t\t\t\treturn !filterTerms.some((term) => keyLower.includes(term))\n\t\t\t})\n\t\t)\n\t}, [chartConfig, filter])\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: not necessary\n\tconst { toolTipFormatter, dataFunction, tickFormatter } = useMemo(() => {\n\t\tconst obj = {} as {\n\t\t\ttoolTipFormatter: (item: any, key: string) => React.ReactNode | string\n\t\t\tdataFunction: (key: string, data: any) => number | null\n\t\t\ttickFormatter: (value: any) => string\n\t\t}\n\t\t// tick formatter\n\t\tif (chartType === ChartType.CPU) {\n\t\t\tobj.tickFormatter = (value) => {\n\t\t\t\tconst val = `${toFixedFloat(value, 2)}%`\n\t\t\t\treturn updateYAxisWidth(val)\n\t\t\t}\n\t\t} else {\n\t\t\tconst chartUnit = isNetChart ? userSettings.unitNet : Unit.Bytes\n\t\t\tobj.tickFormatter = (val) => {\n\t\t\t\tconst { value, unit } = formatBytes(val, isNetChart, chartUnit, !isNetChart)\n\t\t\t\treturn updateYAxisWidth(`${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`)\n\t\t\t}\n\t\t}\n\t\t// tooltip formatter\n\t\tif (isNetChart) {\n\t\t\tconst getRxTxBytes = (record?: { b?: [number, number]; ns?: number; nr?: number }) => {\n\t\t\t\tif (record?.b?.length && record.b.length >= 2) {\n\t\t\t\t\treturn [Number(record.b[0]) || 0, Number(record.b[1]) || 0]\n\t\t\t\t}\n\t\t\t\treturn [(record?.ns ?? 0) * 1024 * 1024, (record?.nr ?? 0) * 1024 * 1024]\n\t\t\t}\n\t\t\tconst formatRxTx = (recv: number, sent: number) => {\n\t\t\t\tconst { value: receivedValue, unit: receivedUnit } = formatBytes(recv, true, userSettings.unitNet, false)\n\t\t\t\tconst { value: sentValue, unit: sentUnit } = formatBytes(sent, true, userSettings.unitNet, false)\n\t\t\t\treturn (\n\t\t\t\t\t<span className=\"flex\">\n\t\t\t\t\t\t{decimalString(receivedValue)} {receivedUnit}\n\t\t\t\t\t\t<span className=\"opacity-70 ms-0.5\"> rx </span>\n\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-3 mx-1.5 bg-primary/40\" />\n\t\t\t\t\t\t{decimalString(sentValue)} {sentUnit}\n\t\t\t\t\t\t<span className=\"opacity-70 ms-0.5\"> tx</span>\n\t\t\t\t\t</span>\n\t\t\t\t)\n\t\t\t}\n\t\t\tobj.toolTipFormatter = (item: any, key: string) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (key === \"__total__\") {\n\t\t\t\t\t\tlet totalSent = 0\n\t\t\t\t\t\tlet totalRecv = 0\n\t\t\t\t\t\tconst payloadData = item?.payload && typeof item.payload === \"object\" ? item.payload : {}\n\t\t\t\t\t\tfor (const [containerKey, value] of Object.entries(payloadData)) {\n\t\t\t\t\t\t\tif (!value || typeof value !== \"object\") {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Skip filtered out containers\n\t\t\t\t\t\t\tif (filteredKeys.has(containerKey)) {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst [sent, recv] = getRxTxBytes(value as { b?: [number, number]; ns?: number; nr?: number })\n\t\t\t\t\t\t\ttotalSent += sent\n\t\t\t\t\t\t\ttotalRecv += recv\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn formatRxTx(totalRecv, totalSent)\n\t\t\t\t\t}\n\t\t\t\t\tconst [sent, recv] = getRxTxBytes(item?.payload?.[key])\n\t\t\t\t\treturn formatRxTx(recv, sent)\n\t\t\t\t} catch (e) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (chartType === ChartType.Memory) {\n\t\t\tobj.toolTipFormatter = (item: any) => {\n\t\t\t\tconst { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)\n\t\t\t\treturn `${decimalString(value)} ${unit}`\n\t\t\t}\n\t\t} else {\n\t\t\tobj.toolTipFormatter = (item: any) => `${decimalString(item.value)}${unit}`\n\t\t}\n\t\t// data function\n\t\tif (isNetChart) {\n\t\t\tobj.dataFunction = (key: string, data: any) => {\n\t\t\t\tconst payload = data[key]\n\t\t\t\tif (!payload) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst sent = payload?.b?.[0] ?? (payload?.ns ?? 0) * 1024 * 1024\n\t\t\t\tconst recv = payload?.b?.[1] ?? (payload?.nr ?? 0) * 1024 * 1024\n\t\t\t\treturn sent + recv\n\t\t\t}\n\t\t} else {\n\t\t\tobj.dataFunction = (key: string, data: any) => data[key]?.[dataKey] ?? null\n\t\t}\n\t\treturn obj\n\t}, [filteredKeys])\n\n\t// console.log('rendered at', new Date())\n\n\tif (containerData.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<AreaChart\n\t\t\t\t\taccessibilityLayer\n\t\t\t\t\t// syncId={'cpu'}\n\t\t\t\t\tdata={containerData}\n\t\t\t\t\tmargin={chartMargin}\n\t\t\t\t\treverseStackOrder={true}\n\t\t\t\t>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\tdomain={pinnedAxisDomain()}\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\ttickFormatter={tickFormatter}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\ttruncate={true}\n\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\titemSorter={(a, b) => b.value - a.value}\n\t\t\t\t\t\tcontent={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} showTotal={true} />}\n\t\t\t\t\t/>\n\t\t\t\t\t{Object.keys(chartConfig).map((key) => {\n\t\t\t\t\t\tconst filtered = filteredKeys.has(key)\n\t\t\t\t\t\tconst fillOpacity = filtered ? 0.05 : 0.4\n\t\t\t\t\t\tconst strokeOpacity = filtered ? 0.1 : 1\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Area\n\t\t\t\t\t\t\t\tkey={key}\n\t\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t\t\tdataKey={dataFunction.bind(null, key)}\n\t\t\t\t\t\t\t\tname={key}\n\t\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\t\tfill={chartConfig[key].color}\n\t\t\t\t\t\t\t\tfillOpacity={fillOpacity}\n\t\t\t\t\t\t\t\tstroke={chartConfig[key].color}\n\t\t\t\t\t\t\t\tstrokeOpacity={strokeOpacity}\n\t\t\t\t\t\t\t\tactiveDot={{ opacity: filtered ? 0 : 1 }}\n\t\t\t\t\t\t\t\tstackId=\"a\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t</AreaChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/disk-chart.tsx",
    "content": "import { useLingui } from \"@lingui/react/macro\"\nimport { memo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from \"@/components/ui/chart\"\nimport { Unit } from \"@/lib/enums\"\nimport { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData, SystemStatsRecord } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function DiskChart({\n\tdataKey,\n\tdiskSize,\n\tchartData,\n}: {\n\tdataKey: string | ((data: SystemStatsRecord) => number | undefined)\n\tdiskSize: number\n\tchartData: ChartData\n}) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\tconst { t } = useLingui()\n\n\t// round to nearest GB\n\tif (diskSize >= 100) {\n\t\tdiskSize = Math.round(diskSize)\n\t}\n\n\tif (chartData.systemStats.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\tdomain={[0, diskSize]}\n\t\t\t\t\t\ttickCount={9}\n\t\t\t\t\t\tminTickGap={6}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val * 1024, false, Unit.Bytes, true)\n\t\t\t\t\t\t\treturn updateYAxisWidth(toFixedFloat(value, value >= 10 ? 0 : 1) + \" \" + unit)\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)\n\t\t\t\t\t\t\t\t\treturn decimalString(convertedValue) + \" \" + unit\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<Area\n\t\t\t\t\t\tdataKey={dataKey}\n\t\t\t\t\t\tname={t`Disk Usage`}\n\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\tfill=\"var(--chart-4)\"\n\t\t\t\t\t\tfillOpacity={0.4}\n\t\t\t\t\t\tstroke=\"var(--chart-4)\"\n\t\t\t\t\t\t// animationDuration={1200}\n\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t/>\n\t\t\t\t</AreaChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/gpu-power-chart.tsx",
    "content": "import { memo, useMemo } from \"react\"\nimport { CartesianGrid, Line, LineChart, YAxis } from \"recharts\"\nimport {\n\tChartContainer,\n\tChartLegend,\n\tChartLegendContent,\n\tChartTooltip,\n\tChartTooltipContent,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData, GPUData } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\nimport type { DataPoint } from \"./line-chart\"\n\nexport default memo(function GpuPowerChart({ chartData }: { chartData: ChartData }) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\tconst packageKey = \" package\"\n\n\tconst { gpuData, dataPoints } = useMemo(() => {\n\t\tconst dataPoints = [] as DataPoint[]\n\t\tconst gpuData = [] as Record<string, GPUData | string>[]\n\t\tconst addedKeys = new Map<string, number>()\n\n\t\tconst addKey = (key: string, value: number) => {\n\t\t\taddedKeys.set(key, (addedKeys.get(key) ?? 0) + value)\n\t\t}\n\n\t\tfor (const stats of chartData.systemStats) {\n\t\t\tconst gpus = stats.stats?.g ?? {}\n\t\t\tconst data = { created: stats.created } as Record<string, GPUData | string>\n\t\t\tfor (const id in gpus) {\n\t\t\t\tconst gpu = gpus[id] as GPUData\n\t\t\t\tdata[gpu.n] = gpu\n\t\t\t\taddKey(gpu.n, gpu.p ?? 0)\n\t\t\t\tif (gpu.pp) {\n\t\t\t\t\tdata[`${gpu.n}${packageKey}`] = gpu\n\t\t\t\t\taddKey(`${gpu.n}${packageKey}`, gpu.pp ?? 0)\n\t\t\t\t}\n\t\t\t}\n\t\t\tgpuData.push(data)\n\t\t}\n\t\tconst sortedKeys = Array.from(addedKeys.entries())\n\t\t\t.sort(([, a], [, b]) => b - a)\n\t\t\t.map(([key]) => key)\n\n\t\tfor (let i = 0; i < sortedKeys.length; i++) {\n\t\t\tconst id = sortedKeys[i]\n\t\t\tdataPoints.push({\n\t\t\t\tlabel: id,\n\t\t\t\tdataKey: (gpuData: Record<string, GPUData>) => {\n\t\t\t\t\treturn id.endsWith(packageKey) ? (gpuData[id]?.pp ?? 0) : (gpuData[id]?.p ?? 0)\n\t\t\t\t},\n\t\t\t\tcolor: `hsl(${226 + (((i * 360) / addedKeys.size) % 360)}, 65%, 52%)`,\n\t\t\t})\n\t\t}\n\t\treturn { gpuData, dataPoints }\n\t}, [chartData])\n\n\tif (chartData.systemStats.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<LineChart accessibilityLayer data={gpuData} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\tdomain={[0, \"auto\"]}\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\ttickFormatter={(value) => {\n\t\t\t\t\t\t\tconst val = toFixedFloat(value, 2)\n\t\t\t\t\t\t\treturn updateYAxisWidth(`${val}W`)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\titemSorter={(a, b) => b.value - a.value}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={(item) => `${decimalString(item.value)}W`}\n\t\t\t\t\t\t\t\t// indicator=\"line\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t{dataPoints.map((dataPoint) => (\n\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\tkey={dataPoint.label}\n\t\t\t\t\t\t\tdataKey={dataPoint.dataKey}\n\t\t\t\t\t\t\tname={dataPoint.label}\n\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\tstrokeWidth={1.5}\n\t\t\t\t\t\t\tstroke={dataPoint.color as string}\n\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t\t{dataPoints.length > 1 && <ChartLegend content={<ChartLegendContent />} />}\n\t\t\t\t</LineChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/hooks.ts",
    "content": "import { useMemo, useState } from \"react\"\nimport type { ChartConfig } from \"@/components/ui/chart\"\nimport type { ChartData, SystemStats, SystemStatsRecord } from \"@/types\"\n\n/** Chart configurations for CPU, memory, and network usage charts */\nexport interface ContainerChartConfigs {\n\tcpu: ChartConfig\n\tmemory: ChartConfig\n\tnetwork: ChartConfig\n}\n\n/**\n * Generates chart configurations for container metrics visualization\n * @param containerData - Array of container statistics data points\n * @returns Chart configurations for CPU, memory, and network metrics\n */\nexport function useContainerChartConfigs(containerData: ChartData[\"containerData\"]): ContainerChartConfigs {\n\treturn useMemo(() => {\n\t\tconst configs = {\n\t\t\tcpu: {} as ChartConfig,\n\t\t\tmemory: {} as ChartConfig,\n\t\t\tnetwork: {} as ChartConfig,\n\t\t}\n\n\t\t// Aggregate usage metrics for each container\n\t\tconst totalUsage = {\n\t\t\tcpu: new Map<string, number>(),\n\t\t\tmemory: new Map<string, number>(),\n\t\t\tnetwork: new Map<string, number>(),\n\t\t}\n\n\t\t// Process each data point to calculate totals\n\t\tfor (let i = 0; i < containerData.length; i++) {\n\t\t\tconst stats = containerData[i]\n\t\t\tconst containerNames = Object.keys(stats)\n\n\t\t\tfor (let j = 0; j < containerNames.length; j++) {\n\t\t\t\tconst containerName = containerNames[j]\n\t\t\t\t// Skip metadata field\n\t\t\t\tif (containerName === \"created\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tconst containerStats = stats[containerName]\n\t\t\t\tif (!containerStats) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Accumulate metrics for CPU, memory, and network\n\t\t\t\tconst currentCpu = totalUsage.cpu.get(containerName) ?? 0\n\t\t\t\tconst currentMemory = totalUsage.memory.get(containerName) ?? 0\n\t\t\t\tconst currentNetwork = totalUsage.network.get(containerName) ?? 0\n\t\t\t\tconst sentBytes = containerStats.b?.[0] ?? (containerStats.ns ?? 0) * 1024 * 1024\n\t\t\t\tconst recvBytes = containerStats.b?.[1] ?? (containerStats.nr ?? 0) * 1024 * 1024\n\n\t\t\t\ttotalUsage.cpu.set(containerName, currentCpu + (containerStats.c ?? 0))\n\t\t\t\ttotalUsage.memory.set(containerName, currentMemory + (containerStats.m ?? 0))\n\t\t\t\ttotalUsage.network.set(containerName, currentNetwork + sentBytes + recvBytes)\n\t\t\t}\n\t\t}\n\n\t\t// Generate chart configurations for each metric type\n\t\tObject.entries(totalUsage).forEach(([chartType, usageMap]) => {\n\t\t\tconst sortedContainers = Array.from(usageMap.entries()).sort(([, a], [, b]) => b - a)\n\t\t\tconst chartConfig = {} as Record<string, { label: string; color: string }>\n\t\t\tconst count = sortedContainers.length\n\n\t\t\t// Generate colors for each container\n\t\t\tfor (let i = 0; i < count; i++) {\n\t\t\t\tconst [containerName] = sortedContainers[i]\n\t\t\t\tconst hue = ((i * 360) / count) % 360\n\t\t\t\tchartConfig[containerName] = {\n\t\t\t\t\tlabel: containerName,\n\t\t\t\t\tcolor: `hsl(${hue}, var(--chart-saturation), var(--chart-lightness))`,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconfigs[chartType as keyof typeof configs] = chartConfig\n\t\t})\n\n\t\treturn configs\n\t}, [containerData])\n}\n\n/** Sets the correct width of the y axis in recharts based on the longest label */\nexport function useYAxisWidth() {\n\tconst [yAxisWidth, setYAxisWidth] = useState(0)\n\tlet maxChars = 0\n\tlet timeout: ReturnType<typeof setTimeout>\n\tfunction updateYAxisWidth(str: string) {\n\t\tif (str.length > maxChars) {\n\t\t\tmaxChars = str.length\n\t\t\tconst div = document.createElement(\"div\")\n\t\t\tdiv.className = \"text-xs tabular-nums tracking-tighter table sr-only\"\n\t\t\tdiv.innerHTML = str\n\t\t\tclearTimeout(timeout)\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\tdocument.body.appendChild(div)\n\t\t\t\tconst width = div.offsetWidth + 24\n\t\t\t\tif (width > yAxisWidth) {\n\t\t\t\t\tsetYAxisWidth(div.offsetWidth + 24)\n\t\t\t\t}\n\t\t\t\tdocument.body.removeChild(div)\n\t\t\t})\n\t\t}\n\t\treturn str\n\t}\n\treturn { yAxisWidth, updateYAxisWidth }\n}\n\n// Assures consistent colors for network interfaces\nexport function useNetworkInterfaces(interfaces: SystemStats[\"ni\"]) {\n\tconst keys = Object.keys(interfaces ?? {})\n\tconst sortedKeys = keys.sort((a, b) => (interfaces?.[b]?.[3] ?? 0) - (interfaces?.[a]?.[3] ?? 0))\n\treturn {\n\t\tlength: sortedKeys.length,\n\t\tdata: (index = 3) => {\n\t\t\treturn sortedKeys.map((key) => ({\n\t\t\t\tlabel: key,\n\t\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.ni?.[key]?.[index],\n\t\t\t\tcolor: `hsl(${220 + (((sortedKeys.indexOf(key) * 360) / sortedKeys.length) % 360)}, 70%, 50%)`,\n\n\t\t\t\topacity: 0.3,\n\t\t\t}))\n\t\t},\n\t}\n}"
  },
  {
    "path": "internal/site/src/components/charts/line-chart.tsx",
    "content": "import { useMemo } from \"react\"\nimport { CartesianGrid, Line, LineChart, YAxis } from \"recharts\"\nimport {\n\tChartContainer,\n\tChartLegend,\n\tChartLegendContent,\n\tChartTooltip,\n\tChartTooltipContent,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { chartMargin, cn, formatShortDate } from \"@/lib/utils\"\nimport type { ChartData, SystemStatsRecord } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport type DataPoint = {\n\tlabel: string\n\tdataKey: (data: SystemStatsRecord) => number | undefined\n\tcolor: number | string\n}\n\nexport default function LineChartDefault({\n\tchartData,\n\tmax,\n\tmaxToggled,\n\ttickFormatter,\n\tcontentFormatter,\n\tdataPoints,\n\tdomain,\n\tlegend,\n\titemSorter,\n}: // logRender = false,\n{\n\tchartData: ChartData\n\tmax?: number\n\tmaxToggled?: boolean\n\ttickFormatter: (value: number, index: number) => string\n\tcontentFormatter: ({ value, payload }: { value: number; payload: SystemStatsRecord }) => string\n\tdataPoints?: DataPoint[]\n\tdomain?: [number, number]\n\tlegend?: boolean\n\titemSorter?: (a: any, b: any) => number\n\t// logRender?: boolean\n}) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: ignore\n\treturn useMemo(() => {\n\t\tif (chartData.systemStats.length === 0) {\n\t\t\treturn null\n\t\t}\n\t\t// if (logRender) {\n\t\t// \tconsole.log(\"Rendered at\", new Date())\n\t\t// }\n\t\treturn (\n\t\t\t<div>\n\t\t\t\t<ChartContainer\n\t\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t\t})}\n\t\t\t\t>\n\t\t\t\t\t<LineChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>\n\t\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\t\tdomain={domain ?? [0, max ?? \"auto\"]}\n\t\t\t\t\t\t\ttickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}\n\t\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\t\titemSorter={itemSorter}\n\t\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\t\tcontentFormatter={contentFormatter}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{dataPoints?.map((dataPoint) => {\n\t\t\t\t\t\t\tlet { color } = dataPoint\n\t\t\t\t\t\t\tif (typeof color === \"number\") {\n\t\t\t\t\t\t\t\tcolor = `var(--chart-${color})`\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\t\tkey={dataPoint.label}\n\t\t\t\t\t\t\t\t\tdataKey={dataPoint.dataKey}\n\t\t\t\t\t\t\t\t\tname={dataPoint.label}\n\t\t\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\t\tstrokeWidth={1.5}\n\t\t\t\t\t\t\t\t\tstroke={color}\n\t\t\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{legend && <ChartLegend content={<ChartLegendContent />} />}\n\t\t\t\t\t</LineChart>\n\t\t\t\t</ChartContainer>\n\t\t\t</div>\n\t\t)\n\t}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])\n}\n"
  },
  {
    "path": "internal/site/src/components/charts/load-average-chart.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { memo } from \"react\"\nimport { CartesianGrid, Line, LineChart, YAxis } from \"recharts\"\nimport {\n\tChartContainer,\n\tChartLegend,\n\tChartLegendContent,\n\tChartTooltip,\n\tChartTooltipContent,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { chartMargin, cn, decimalString, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData, SystemStats } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\n\tconst keys: { color: string; label: string }[] = [\n\t\t{\n\t\t\tcolor: \"hsl(271, 81%, 60%)\", // Purple\n\t\t\tlabel: t({ message: `1 min`, comment: \"Load average\" }),\n\t\t},\n\t\t{\n\t\t\tcolor: \"hsl(217, 91%, 60%)\", // Blue\n\t\t\tlabel: t({ message: `5 min`, comment: \"Load average\" }),\n\t\t},\n\t\t{\n\t\t\tcolor: \"hsl(25, 95%, 53%)\", // Orange\n\t\t\tlabel: t({ message: `15 min`, comment: \"Load average\" }),\n\t\t},\n\t]\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<LineChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\tdomain={[0, \"auto\"]}\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\ttickFormatter={(value) => {\n\t\t\t\t\t\t\treturn updateYAxisWidth(String(toFixedFloat(value, 2)))\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={(item) => decimalString(item.value)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t{keys.map(({ color, label }, i) => (\n\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\tkey={label}\n\t\t\t\t\t\t\tdataKey={(value: { stats: SystemStats }) => value.stats?.la?.[i]}\n\t\t\t\t\t\t\tname={label}\n\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\tstrokeWidth={1.5}\n\t\t\t\t\t\t\tstroke={color}\n\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t\t<ChartLegend content={<ChartLegendContent />} />\n\t\t\t\t</LineChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/mem-chart.tsx",
    "content": "import { useLingui } from \"@lingui/react/macro\"\nimport { memo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from \"@/components/ui/chart\"\nimport { Unit } from \"@/lib/enums\"\nimport { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\tconst { t } = useLingui()\n\n\tconst totalMem = toFixedFloat(chartData.systemStats.at(-1)?.stats.m ?? 0, 1)\n\n\t// console.log('rendered at', new Date())\n\n\tif (chartData.systemStats.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t{/* {!yAxisSet && <Spinner />} */}\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t{totalMem && (\n\t\t\t\t\t\t<YAxis\n\t\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\t\t// use \"ticks\" instead of domain / tickcount if need more control\n\t\t\t\t\t\t\tdomain={[0, totalMem]}\n\t\t\t\t\t\t\ttickCount={9}\n\t\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t\t\ttickFormatter={(value) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)\n\t\t\t\t\t\t\t\treturn updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + \" \" + unit)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\t// cursor={false}\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\t\t\titemSorter={(a, b) => a.order - b.order}\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\t\t// mem values are supplied as GB\n\t\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)\n\t\t\t\t\t\t\t\t\treturn decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + \" \" + unit\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tshowTotal={true}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<Area\n\t\t\t\t\t\tname={t`Used`}\n\t\t\t\t\t\torder={3}\n\t\t\t\t\t\tdataKey={({ stats }) => (showMax ? stats?.mm : stats?.mu)}\n\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\tfill=\"var(--chart-2)\"\n\t\t\t\t\t\tfillOpacity={0.4}\n\t\t\t\t\t\tstroke=\"var(--chart-2)\"\n\t\t\t\t\t\tstackId=\"1\"\n\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{/* {chartData.systemStats.at(-1)?.stats.mz && ( */}\n\t\t\t\t\t<Area\n\t\t\t\t\t\tname=\"ZFS ARC\"\n\t\t\t\t\t\torder={2}\n\t\t\t\t\t\tdataKey={({ stats }) => (showMax ? null : stats?.mz)}\n\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\tfill=\"hsla(175 60% 45% / 0.8)\"\n\t\t\t\t\t\tfillOpacity={0.5}\n\t\t\t\t\t\tstroke=\"hsla(175 60% 45% / 0.8)\"\n\t\t\t\t\t\tstackId=\"1\"\n\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{/* )} */}\n\t\t\t\t\t<Area\n\t\t\t\t\t\tname={t`Cache / Buffers`}\n\t\t\t\t\t\torder={1}\n\t\t\t\t\t\tdataKey={({ stats }) => (showMax ? null : stats?.mb)}\n\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\tfill=\"hsla(160 60% 45% / 0.5)\"\n\t\t\t\t\t\tfillOpacity={0.4}\n\t\t\t\t\t\tstroke=\"hsla(160 60% 45% / 0.5)\"\n\t\t\t\t\t\tstackId=\"1\"\n\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{/* <ChartLegend content={<ChartLegendContent />} /> */}\n\t\t\t\t</AreaChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/swap-chart.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { memo } from \"react\"\nimport { Area, AreaChart, CartesianGrid, YAxis } from \"recharts\"\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, xAxis } from \"@/components/ui/chart\"\nimport { $userSettings } from \"@/lib/stores\"\nimport { chartMargin, cn, decimalString, formatBytes, formatShortDate, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function SwapChart({ chartData }: { chartData: ChartData }) {\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\tconst userSettings = useStore($userSettings)\n\n\tif (chartData.systemStats.length === 0) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<AreaChart accessibilityLayer data={chartData.systemStats} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\tdomain={[0, () => toFixedFloat(chartData.systemStats.at(-1)?.stats.s ?? 0.04, 2)]}\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t\ttickFormatter={(value) => {\n\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)\n\t\t\t\t\t\t\treturn updateYAxisWidth(toFixedFloat(convertedValue, value >= 10 ? 0 : 1) + \" \" + unit)\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\t\t// mem values are supplied as GB\n\t\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value * 1024, false, userSettings.unitDisk, true)\n\t\t\t\t\t\t\t\t\treturn decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + \" \" + unit\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t// indicator=\"line\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<Area\n\t\t\t\t\t\tdataKey=\"stats.su\"\n\t\t\t\t\t\tname={t`Used`}\n\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\tfill=\"var(--chart-2)\"\n\t\t\t\t\t\tfillOpacity={0.4}\n\t\t\t\t\t\tstroke=\"var(--chart-2)\"\n\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t/>\n\t\t\t\t</AreaChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/charts/temperature-chart.tsx",
    "content": "import { useStore } from \"@nanostores/react\"\nimport { memo, useMemo } from \"react\"\nimport { CartesianGrid, Line, LineChart, YAxis } from \"recharts\"\nimport {\n\tChartContainer,\n\tChartLegend,\n\tChartLegendContent,\n\tChartTooltip,\n\tChartTooltipContent,\n\txAxis,\n} from \"@/components/ui/chart\"\nimport { $temperatureFilter, $userSettings } from \"@/lib/stores\"\nimport { chartMargin, cn, decimalString, formatShortDate, formatTemperature, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { useYAxisWidth } from \"./hooks\"\n\nexport default memo(function TemperatureChart({ chartData }: { chartData: ChartData }) {\n\tconst filter = useStore($temperatureFilter)\n\tconst userSettings = useStore($userSettings)\n\tconst { yAxisWidth, updateYAxisWidth } = useYAxisWidth()\n\n\tif (chartData.systemStats.length === 0) {\n\t\treturn null\n\t}\n\n\t/** Format temperature data for chart and assign colors */\n\tconst newChartData = useMemo(() => {\n\t\tconst newChartData = { data: [], colors: {} } as {\n\t\t\tdata: Record<string, number | string>[]\n\t\t\tcolors: Record<string, string>\n\t\t}\n\t\tconst tempSums = {} as Record<string, number>\n\t\tfor (const data of chartData.systemStats) {\n\t\t\tconst newData = { created: data.created } as Record<string, number | string>\n\t\t\tconst keys = Object.keys(data.stats?.t ?? {})\n\t\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\t\tconst key = keys[i]\n\t\t\t\tnewData[key] = data.stats.t![key]\n\t\t\t\ttempSums[key] = (tempSums[key] ?? 0) + newData[key]\n\t\t\t}\n\t\t\tnewChartData.data.push(newData)\n\t\t}\n\t\tconst keys = Object.keys(tempSums).sort((a, b) => tempSums[b] - tempSums[a])\n\t\tfor (const key of keys) {\n\t\t\tnewChartData.colors[key] = `hsl(${((keys.indexOf(key) * 360) / keys.length) % 360}, 60%, 55%)`\n\t\t}\n\t\treturn newChartData\n\t}, [chartData])\n\n\tconst colors = Object.keys(newChartData.colors)\n\n\t// console.log('rendered at', new Date())\n\n\treturn (\n\t\t<div>\n\t\t\t<ChartContainer\n\t\t\t\tclassName={cn(\"h-full w-full absolute aspect-auto bg-card opacity-0 transition-opacity\", {\n\t\t\t\t\t\"opacity-100\": yAxisWidth,\n\t\t\t\t})}\n\t\t\t>\n\t\t\t\t<LineChart accessibilityLayer data={newChartData.data} margin={chartMargin}>\n\t\t\t\t\t<CartesianGrid vertical={false} />\n\t\t\t\t\t<YAxis\n\t\t\t\t\t\tdirection=\"ltr\"\n\t\t\t\t\t\torientation={chartData.orientation}\n\t\t\t\t\t\tclassName=\"tracking-tighter\"\n\t\t\t\t\t\tdomain={[\"auto\", \"auto\"]}\n\t\t\t\t\t\twidth={yAxisWidth}\n\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\tconst { value, unit } = formatTemperature(val, userSettings.unitTemp)\n\t\t\t\t\t\t\treturn updateYAxisWidth(toFixedFloat(value, 2) + \" \" + unit)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\ttickLine={false}\n\t\t\t\t\t\taxisLine={false}\n\t\t\t\t\t/>\n\t\t\t\t\t{xAxis(chartData)}\n\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\tanimationEasing=\"ease-out\"\n\t\t\t\t\t\tanimationDuration={150}\n\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\titemSorter={(a, b) => b.value - a.value}\n\t\t\t\t\t\tcontent={\n\t\t\t\t\t\t\t<ChartTooltipContent\n\t\t\t\t\t\t\t\tlabelFormatter={(_, data) => formatShortDate(data[0].payload.created)}\n\t\t\t\t\t\t\t\tcontentFormatter={(item) => {\n\t\t\t\t\t\t\t\t\tconst { value, unit } = formatTemperature(item.value, userSettings.unitTemp)\n\t\t\t\t\t\t\t\t\treturn decimalString(value) + \" \" + unit\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tfilter={filter}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t{colors.map((key) => {\n\t\t\t\t\t\tconst filterTerms = filter ? filter.toLowerCase().split(\" \").filter(term => term.length > 0) : []\n\t\t\t\t\t\tconst filtered = filterTerms.length > 0 && !filterTerms.some(term => key.toLowerCase().includes(term))\n\t\t\t\t\t\tconst strokeOpacity = filtered ? 0.1 : 1\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Line\n\t\t\t\t\t\t\t\tkey={key}\n\t\t\t\t\t\t\t\tdataKey={key}\n\t\t\t\t\t\t\t\tname={key}\n\t\t\t\t\t\t\t\ttype=\"monotoneX\"\n\t\t\t\t\t\t\t\tdot={false}\n\t\t\t\t\t\t\t\tstrokeWidth={1.5}\n\t\t\t\t\t\t\t\tstroke={newChartData.colors[key]}\n\t\t\t\t\t\t\t\tstrokeOpacity={strokeOpacity}\n\t\t\t\t\t\t\t\tactiveDot={{ opacity: filtered ? 0 : 1 }}\n\t\t\t\t\t\t\t\tisAnimationActive={false}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{colors.length < 12 && <ChartLegend content={<ChartLegendContent />} />}\n\t\t\t\t</LineChart>\n\t\t\t</ChartContainer>\n\t\t</div>\n\t)\n})"
  },
  {
    "path": "internal/site/src/components/command-palette.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { DialogDescription } from \"@radix-ui/react-dialog\"\nimport {\n\tAlertOctagonIcon,\n\tBookIcon,\n\tContainerIcon,\n\tDatabaseBackupIcon,\n\tFingerprintIcon,\n\tHardDriveIcon,\n\tLogsIcon,\n\tMailIcon,\n\tServer,\n\tServerIcon,\n\tSettingsIcon,\n\tUsersIcon,\n} from \"lucide-react\"\nimport { memo, useEffect, useMemo } from \"react\"\nimport {\n\tCommandDialog,\n\tCommandEmpty,\n\tCommandGroup,\n\tCommandInput,\n\tCommandItem,\n\tCommandList,\n\tCommandSeparator,\n\tCommandShortcut,\n} from \"@/components/ui/command\"\nimport { isAdmin } from \"@/lib/api\"\nimport { $systems } from \"@/lib/stores\"\nimport { getHostDisplayValue, listen } from \"@/lib/utils\"\nimport { $router, basePath, navigate, prependBasePath } from \"./router\"\n\nexport default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {\n\tuseEffect(() => {\n\t\tconst down = (e: KeyboardEvent) => {\n\t\t\tif (e.key === \"k\" && (e.metaKey || e.ctrlKey)) {\n\t\t\t\te.preventDefault()\n\t\t\t\tsetOpen(!open)\n\t\t\t}\n\t\t}\n\t\treturn listen(document, \"keydown\", down)\n\t}, [open, setOpen])\n\n\treturn useMemo(() => {\n\t\tconst systems = $systems.get()\n\t\tconst SettingsShortcut = (\n\t\t\t<CommandShortcut>\n\t\t\t\t<Trans>Settings</Trans>\n\t\t\t</CommandShortcut>\n\t\t)\n\t\tconst AdminShortcut = (\n\t\t\t<CommandShortcut>\n\t\t\t\t<Trans>Admin</Trans>\n\t\t\t</CommandShortcut>\n\t\t)\n\t\treturn (\n\t\t\t<CommandDialog open={open} onOpenChange={setOpen}>\n\t\t\t\t<DialogDescription className=\"sr-only\">Command palette</DialogDescription>\n\t\t\t\t<CommandInput placeholder={t`Search for systems or settings...`} />\n\t\t\t\t<CommandList>\n\t\t\t\t\t{systems.length > 0 && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<CommandGroup>\n\t\t\t\t\t\t\t\t{systems.map((system) => (\n\t\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\t\tkey={system.id}\n\t\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"system\", { id: system.id }))\n\t\t\t\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Server className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t\t<span className=\"max-w-60 truncate\">{system.name}</span>\n\t\t\t\t\t\t\t\t\t\t<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>\n\t\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</CommandGroup>\n\t\t\t\t\t\t\t<CommandSeparator className=\"mb-1.5\" />\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t\t<CommandGroup heading={t`Pages / Settings`}>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tkeywords={[\"home\"]}\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(basePath)\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ServerIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>All Systems</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<CommandShortcut>\n\t\t\t\t\t\t\t\t<Trans>Page</Trans>\n\t\t\t\t\t\t\t</CommandShortcut>\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"containers\"))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ContainerIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>All Containers</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<CommandShortcut>\n\t\t\t\t\t\t\t\t<Trans>Page</Trans>\n\t\t\t\t\t\t\t</CommandShortcut>\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"smart\"))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<HardDriveIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>S.M.A.R.T.</span>\n\t\t\t\t\t\t\t<CommandShortcut>\n\t\t\t\t\t\t\t\t<Trans>Page</Trans>\n\t\t\t\t\t\t\t</CommandShortcut>\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"settings\", { name: \"general\" }))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<SettingsIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Settings</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{SettingsShortcut}\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tkeywords={[\"alerts\"]}\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"settings\", { name: \"notifications\" }))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<MailIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Notifications</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{SettingsShortcut}\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tkeywords={[t`Universal token`]}\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"settings\", { name: \"tokens\" }))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<FingerprintIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Tokens & Fingerprints</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{SettingsShortcut}\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\tnavigate(getPagePath($router, \"settings\", { name: \"alert-history\" }))\n\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AlertOctagonIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Alert History</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{SettingsShortcut}\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\tkeywords={[\"help\", \"oauth\", \"oidc\"]}\n\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\twindow.location.href = \"https://beszel.dev/guide/what-is-beszel\"\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<BookIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Documentation</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<CommandShortcut>beszel.dev</CommandShortcut>\n\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t</CommandGroup>\n\t\t\t\t\t{isAdmin() && (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<CommandSeparator className=\"mb-1.5\" />\n\t\t\t\t\t\t\t<CommandGroup heading={t`Admin`}>\n\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\tkeywords={[\"pocketbase\"]}\n\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\t\t\twindow.open(prependBasePath(\"/_/\"), \"_blank\")\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<UsersIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t<Trans>Users</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{AdminShortcut}\n\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\t\t\twindow.open(prependBasePath(\"/_/#/logs\"), \"_blank\")\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<LogsIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t<Trans>Logs</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{AdminShortcut}\n\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\t\t\twindow.open(prependBasePath(\"/_/#/settings/backups\"), \"_blank\")\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<DatabaseBackupIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t<Trans>Backups</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{AdminShortcut}\n\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\tkeywords={[\"email\"]}\n\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\tsetOpen(false)\n\t\t\t\t\t\t\t\t\t\twindow.open(prependBasePath(\"/_/#/settings/mail\"), \"_blank\")\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<MailIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t<Trans>SMTP settings</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{AdminShortcut}\n\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t</CommandGroup>\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t\t<CommandEmpty>\n\t\t\t\t\t\t<Trans>No results found.</Trans>\n\t\t\t\t\t</CommandEmpty>\n\t\t\t\t</CommandList>\n\t\t\t</CommandDialog>\n\t\t)\n\t}, [open])\n})\n"
  },
  {
    "path": "internal/site/src/components/containers-table/containers-table-columns.tsx",
    "content": "import type { Column, ColumnDef } from \"@tanstack/react-table\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn, decimalString, formatBytes, hourWithSeconds } from \"@/lib/utils\"\nimport type { ContainerRecord } from \"@/types\"\nimport { ContainerHealth, ContainerHealthLabels } from \"@/lib/enums\"\nimport {\n\tClockIcon,\n\tContainerIcon,\n\tCpuIcon,\n\tLayersIcon,\n\tMemoryStickIcon,\n\tServerIcon,\n\tShieldCheckIcon,\n} from \"lucide-react\"\nimport { EthernetIcon, HourglassIcon, SquareArrowRightEnterIcon } from \"../ui/icons\"\nimport { Badge } from \"../ui/badge\"\nimport { t } from \"@lingui/core/macro\"\nimport { $allSystemsById, $longestSystemNameLen } from \"@/lib/stores\"\nimport { useStore } from \"@nanostores/react\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\"\n\n// Unit names and their corresponding number of seconds for converting docker status strings\nconst unitSeconds = [\n\t[\"s\", 1],\n\t[\"mi\", 60],\n\t[\"h\", 3600],\n\t[\"d\", 86400],\n\t[\"w\", 604800],\n\t[\"mo\", 2592000],\n] as const\n// Convert docker status string to number of seconds (\"Up X minutes\", \"Up X hours\", etc.)\nfunction getStatusValue(status: string): number {\n\tconst [_, num, unit] = status.split(\" \")\n\t// Docker uses \"a\" or \"an\" instead of \"1\" for singular units (e.g., \"Up a minute\", \"Up an hour\")\n\tconst numValue = num === \"a\" || num === \"an\" ? 1 : Number(num)\n\tfor (const [unitName, value] of unitSeconds) {\n\t\tif (unit.startsWith(unitName)) {\n\t\t\treturn numValue * value\n\t\t}\n\t}\n\treturn 0\n}\n\nexport const containerChartCols: ColumnDef<ContainerRecord>[] = [\n\t{\n\t\tid: \"name\",\n\t\tsortingFn: (a, b) => a.original.name.localeCompare(b.original.name),\n\t\taccessorFn: (record) => record.name,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={ContainerIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\treturn <span className=\"ms-1.5 xl:w-48 block truncate\">{getValue() as string}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"system\",\n\t\taccessorFn: (record) => record.system,\n\t\tsortingFn: (a, b) => {\n\t\t\tconst allSystems = $allSystemsById.get()\n\t\t\tconst systemNameA = allSystems[a.original.system]?.name ?? \"\"\n\t\t\tconst systemNameB = allSystems[b.original.system]?.name ?? \"\"\n\t\t\treturn systemNameA.localeCompare(systemNameB)\n\t\t},\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst allSystems = useStore($allSystemsById)\n\t\t\tconst longestName = useStore($longestSystemNameLen)\n\t\t\treturn (\n\t\t\t\t<div className=\"ms-1 max-w-40 truncate\" style={{ width: `${longestName / 1.05}ch` }}>\n\t\t\t\t\t{allSystems[getValue() as string]?.name ?? \"\"}\n\t\t\t\t</div>\n\t\t\t)\n\t\t},\n\t},\n\t// {\n\t// \tid: \"id\",\n\t// \taccessorFn: (record) => record.id,\n\t// \tsortingFn: (a, b) => a.original.id.localeCompare(b.original.id),\n\t// \theader: ({ column }) => <HeaderButton column={column} name=\"ID\" Icon={HashIcon} />,\n\t// \tcell: ({ getValue }) => {\n\t// \t\treturn <span className=\"ms-1.5 me-3 font-mono\">{getValue() as string}</span>\n\t// \t},\n\t// },\n\t{\n\t\tid: \"cpu\",\n\t\taccessorFn: (record) => record.cpu,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`CPU`} Icon={CpuIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\treturn <span className=\"ms-1 tabular-nums\">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"memory\",\n\t\taccessorFn: (record) => record.memory,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Memory`} Icon={MemoryStickIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tconst formatted = formatBytes(val, false, undefined, true)\n\t\t\treturn (\n\t\t\t\t<span className=\"ms-1 tabular-nums\">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"net\",\n\t\taccessorFn: (record) => record.net,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Net`} Icon={EthernetIcon} />,\n\t\tminSize: 112,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tconst formatted = formatBytes(val, true, undefined, false)\n\t\t\treturn (\n\t\t\t\t<div className=\"ms-1 tabular-nums\">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</div>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"health\",\n\t\tinvertSorting: true,\n\t\taccessorFn: (record) => record.health,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Health`} Icon={ShieldCheckIcon} />,\n\t\tminSize: 121,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst healthValue = getValue() as number\n\t\t\tconst healthStatus = ContainerHealthLabels[healthValue] || \"Unknown\"\n\t\t\treturn (\n\t\t\t\t<Badge variant=\"outline\" className=\"dark:border-white/12\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName={cn(\"size-2 me-1.5 rounded-full\", {\n\t\t\t\t\t\t\t\"bg-green-500\": healthValue === ContainerHealth.Healthy,\n\t\t\t\t\t\t\t\"bg-red-500\": healthValue === ContainerHealth.Unhealthy,\n\t\t\t\t\t\t\t\"bg-yellow-500\": healthValue === ContainerHealth.Starting,\n\t\t\t\t\t\t\t\"bg-zinc-500\": healthValue === ContainerHealth.None,\n\t\t\t\t\t\t})}\n\t\t\t\t\t></span>\n\t\t\t\t\t{healthStatus}\n\t\t\t\t</Badge>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"ports\",\n\t\taccessorFn: (record) => record.ports || undefined,\n\t\theader: ({ column }) => (\n\t\t\t<HeaderButton\n\t\t\t\tcolumn={column}\n\t\t\t\tname={t({ message: \"Ports\", context: \"Container ports\" })}\n\t\t\t\tIcon={SquareArrowRightEnterIcon}\n\t\t\t/>\n\t\t),\n\t\tsortingFn: (a, b) => getPortValue(a.original.ports) - getPortValue(b.original.ports),\n\t\tminSize: 147,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as string | undefined\n\t\t\tif (!val) {\n\t\t\t\treturn <div className=\"ms-1.5 text-muted-foreground\">-</div>\n\t\t\t}\n\t\t\tconst className = \"ms-1 w-27 block truncate tabular-nums\"\n\t\t\tif (val.length > 14) {\n\t\t\t\treturn (\n\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t<TooltipTrigger className={className}>{val}</TooltipTrigger>\n\t\t\t\t\t\t<TooltipContent>{val}</TooltipContent>\n\t\t\t\t\t</Tooltip>\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn <span className={className}>{val}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"image\",\n\t\tsortingFn: (a, b) => a.original.image.localeCompare(b.original.image),\n\t\taccessorFn: (record) => record.image,\n\t\theader: ({ column }) => (\n\t\t\t<HeaderButton column={column} name={t({ message: \"Image\", context: \"Docker image\" })} Icon={LayersIcon} />\n\t\t),\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as string\n\t\t\treturn (\n\t\t\t\t<div className=\"ms-1 xl:w-40 truncate\" title={val}>\n\t\t\t\t\t{val}\n\t\t\t\t</div>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"status\",\n\t\taccessorFn: (record) => record.status,\n\t\tinvertSorting: true,\n\t\tsortingFn: (a, b) => getStatusValue(a.original.status) - getStatusValue(b.original.status),\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={HourglassIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\treturn <span className=\"ms-1 w-25 block truncate\">{getValue() as string}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"updated\",\n\t\tinvertSorting: true,\n\t\taccessorFn: (record) => record.updated,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst timestamp = getValue() as number\n\t\t\treturn <span className=\"ms-1 tabular-nums\">{hourWithSeconds(new Date(timestamp).toISOString())}</span>\n\t\t},\n\t},\n]\n\nfunction HeaderButton({\n\tcolumn,\n\tname,\n\tIcon,\n}: {\n\tcolumn: Column<ContainerRecord>\n\tname: string\n\tIcon: React.ElementType\n}) {\n\tconst isSorted = column.getIsSorted()\n\treturn (\n\t\t<Button\n\t\t\tclassName={cn(\n\t\t\t\t\"h-9 px-3 flex items-center gap-2 duration-50\",\n\t\t\t\tisSorted && \"bg-accent/70 light:bg-accent text-accent-foreground/90\"\n\t\t\t)}\n\t\t\tvariant=\"ghost\"\n\t\t\tonClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n\t\t>\n\t\t\t{Icon && <Icon className=\"size-4\" />}\n\t\t\t{name}\n\t\t\t{/* <ArrowUpDownIcon className=\"size-4\" /> */}\n\t\t</Button>\n\t)\n}\n\n/**\n * Convert port string to a number for sorting.\n * Handles formats like \"80\", \"127.0.0.1:80\", and \"80, 443\" (takes the first mapping).\n */\nfunction getPortValue(ports: string | undefined): number {\n\tif (!ports) {\n\t\treturn 0\n\t}\n\tconst first = ports.includes(\",\") ? ports.substring(0, ports.indexOf(\",\")) : ports\n\tconst colonIndex = first.lastIndexOf(\":\")\n\tconst portStr = colonIndex === -1 ? first : first.substring(colonIndex + 1)\n\treturn Number(portStr) || 0\n}\n"
  },
  {
    "path": "internal/site/src/components/containers-table/containers-table.tsx",
    "content": "/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: html comes directly from docker via agent */\nimport { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport {\n\ttype ColumnFiltersState,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetFilteredRowModel,\n\tgetSortedRowModel,\n\ttype Row,\n\ttype SortingState,\n\ttype Table as TableType,\n\tuseReactTable,\n\ttype VisibilityState,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer, type VirtualItem } from \"@tanstack/react-virtual\"\nimport { memo, type RefObject, useEffect, useRef, useState } from \"react\"\nimport { Input } from \"@/components/ui/input\"\nimport { TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { pb } from \"@/lib/api\"\nimport type { ContainerRecord } from \"@/types\"\nimport { containerChartCols } from \"@/components/containers-table/containers-table-columns\"\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { type ContainerHealth, ContainerHealthLabels } from \"@/lib/enums\"\nimport { cn, useBrowserStorage } from \"@/lib/utils\"\nimport { Sheet, SheetTitle, SheetHeader, SheetContent, SheetDescription } from \"../ui/sheet\"\nimport { Dialog, DialogContent, DialogTitle } from \"../ui/dialog\"\nimport { Button } from \"@/components/ui/button\"\nimport { $allSystemsById } from \"@/lib/stores\"\nimport { LoaderCircleIcon, MaximizeIcon, RefreshCwIcon, XIcon } from \"lucide-react\"\nimport { Separator } from \"../ui/separator\"\nimport { $router, Link } from \"../router\"\nimport { listenKeys } from \"nanostores\"\nimport { getPagePath } from \"@nanostores/router\"\n\nconst syntaxTheme = \"github-dark-dimmed\"\n\nexport default function ContainersTable({ systemId }: { systemId?: string }) {\n\tconst loadTime = Date.now()\n\tconst [data, setData] = useState<ContainerRecord[] | undefined>(undefined)\n\tconst [sorting, setSorting] = useBrowserStorage<SortingState>(\n\t\t`sort-c-${systemId ? 1 : 0}`,\n\t\t[{ id: systemId ? \"name\" : \"system\", desc: false }],\n\t\tsessionStorage\n\t)\n\tconst [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n\tconst [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})\n\n\t// Hide ports column if no ports are present\n\tuseEffect(() => {\n\t\tif (data) {\n\t\t\tconst hasPorts = data.some((container) => container.ports)\n\t\t\tsetColumnVisibility((prev) => {\n\t\t\t\tif (prev.ports === hasPorts) {\n\t\t\t\t\treturn prev\n\t\t\t\t}\n\t\t\t\treturn { ...prev, ports: hasPorts }\n\t\t\t})\n\t\t}\n\t}, [data])\n\n\tconst [rowSelection, setRowSelection] = useState({})\n\tconst [globalFilter, setGlobalFilter] = useState(\"\")\n\n\tuseEffect(() => {\n\t\tfunction fetchData(systemId?: string) {\n\t\t\tpb.collection<ContainerRecord>(\"containers\")\n\t\t\t\t.getList(0, 2000, {\n\t\t\t\t\tfields: \"id,name,image,ports,cpu,memory,net,health,status,system,updated\",\n\t\t\t\t\tfilter: systemId ? pb.filter(\"system={:system}\", { system: systemId }) : undefined,\n\t\t\t\t})\n\t\t\t\t.then(({ items }) => {\n\t\t\t\t\tif (items.length === 0) {\n\t\t\t\t\t\tsetData((curItems) => {\n\t\t\t\t\t\t\tif (systemId) {\n\t\t\t\t\t\t\t\treturn curItems?.filter((item) => item.system !== systemId) ?? []\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn []\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tsetData((curItems) => {\n\t\t\t\t\t\tconst lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)\n\t\t\t\t\t\tconst containerIds = new Set()\n\t\t\t\t\t\tconst newItems: ContainerRecord[] = []\n\t\t\t\t\t\tfor (const item of items) {\n\t\t\t\t\t\t\tif (Math.abs(lastUpdated - item.updated) < 70_000) {\n\t\t\t\t\t\t\t\tcontainerIds.add(item.id)\n\t\t\t\t\t\t\t\tnewItems.push(item)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfor (const item of curItems ?? []) {\n\t\t\t\t\t\t\tif (!containerIds.has(item.id) && lastUpdated - item.updated < 70_000) {\n\t\t\t\t\t\t\t\tnewItems.push(item)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn newItems\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t}\n\n\t\t// initial load\n\t\tfetchData(systemId)\n\n\t\t// if no systemId, pull system containers after every system update\n\t\tif (!systemId) {\n\t\t\treturn $allSystemsById.listen((_value, _oldValue, systemId) => {\n\t\t\t\t// exclude initial load of systems\n\t\t\t\tif (Date.now() - loadTime > 500) {\n\t\t\t\t\tfetchData(systemId)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\t// if systemId, fetch containers after the system is updated\n\t\treturn listenKeys($allSystemsById, [systemId], (_newSystems) => {\n\t\t\tfetchData(systemId)\n\t\t})\n\t}, [])\n\n\tconst table = useReactTable({\n\t\tdata: data ?? [],\n\t\tcolumns: containerChartCols.filter((col) => (systemId ? col.id !== \"system\" : true)),\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tonSortingChange: setSorting,\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tonColumnVisibilityChange: setColumnVisibility,\n\t\tonRowSelectionChange: setRowSelection,\n\t\tdefaultColumn: {\n\t\t\tsortUndefined: \"last\",\n\t\t\tsize: 100,\n\t\t\tminSize: 0,\n\t\t},\n\t\tstate: {\n\t\t\tsorting,\n\t\t\tcolumnFilters,\n\t\t\tcolumnVisibility,\n\t\t\trowSelection,\n\t\t\tglobalFilter,\n\t\t},\n\t\tonGlobalFilterChange: setGlobalFilter,\n\t\tglobalFilterFn: (row, _columnId, filterValue) => {\n\t\t\tconst container = row.original\n\t\t\tconst systemName = $allSystemsById.get()[container.system]?.name ?? \"\"\n\t\t\tconst id = container.id ?? \"\"\n\t\t\tconst name = container.name ?? \"\"\n\t\t\tconst status = container.status ?? \"\"\n\t\t\tconst healthLabel = ContainerHealthLabels[container.health as ContainerHealth] ?? \"\"\n\t\t\tconst image = container.image ?? \"\"\n\t\t\tconst ports = container.ports ?? \"\"\n\t\t\tconst searchString = `${systemName} ${id} ${name} ${healthLabel} ${status} ${image} ${ports}`.toLowerCase()\n\n\t\t\treturn (filterValue as string)\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(\" \")\n\t\t\t\t.every((term) => searchString.includes(term))\n\t\t},\n\t})\n\n\tconst rows = table.getRowModel().rows\n\tconst visibleColumns = table.getVisibleLeafColumns()\n\n\treturn (\n\t\t<Card className=\"p-6 @container w-full\">\n\t\t\t<CardHeader className=\"p-0 mb-4\">\n\t\t\t\t<div className=\"grid md:flex gap-5 w-full items-end\">\n\t\t\t\t\t<div className=\"px-2 sm:px-1\">\n\t\t\t\t\t\t<CardTitle className=\"mb-2\">\n\t\t\t\t\t\t\t<Trans>All Containers</Trans>\n\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t<CardDescription className=\"flex\">\n\t\t\t\t\t\t\t<Trans>Click on a container to view more information.</Trans>\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"relative ms-auto w-full max-w-full md:w-64\">\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\t\t\t\tvalue={globalFilter}\n\t\t\t\t\t\t\tonChange={(e) => setGlobalFilter(e.target.value)}\n\t\t\t\t\t\t\tclassName=\"ps-4 pe-10 w-full\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{globalFilter && (\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\taria-label={t`Clear`}\n\t\t\t\t\t\t\t\tclassName=\"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground\"\n\t\t\t\t\t\t\t\tonClick={() => setGlobalFilter(\"\")}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<XIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</CardHeader>\n\t\t\t<div className=\"rounded-md\">\n\t\t\t\t<AllContainersTable table={table} rows={rows} colLength={visibleColumns.length} data={data} />\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n\nconst AllContainersTable = memo(function AllContainersTable({\n\ttable,\n\trows,\n\tcolLength,\n\tdata,\n}: {\n\ttable: TableType<ContainerRecord>\n\trows: Row<ContainerRecord>[]\n\tcolLength: number\n\tdata: ContainerRecord[] | undefined\n}) {\n\t// The virtualizer will need a reference to the scrollable container element\n\tconst scrollRef = useRef<HTMLDivElement>(null)\n\tconst activeContainer = useRef<ContainerRecord | null>(null)\n\tconst [sheetOpen, setSheetOpen] = useState(false)\n\tconst openSheet = (container: ContainerRecord) => {\n\t\tactiveContainer.current = container\n\t\tsetSheetOpen(true)\n\t}\n\n\tconst virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n\t\tcount: rows.length,\n\t\testimateSize: () => 54,\n\t\tgetScrollElement: () => scrollRef.current,\n\t\toverscan: 5,\n\t})\n\tconst virtualRows = virtualizer.getVirtualItems()\n\n\tconst paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)\n\tconst paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md\",\n\t\t\t\t// don't set min height if there are less than 2 rows, do set if we need to display the empty state\n\t\t\t\t(!rows.length || rows.length > 2) && \"min-h-50\"\n\t\t\t)}\n\t\t\tref={scrollRef}\n\t\t>\n\t\t\t{/* add header height to table size */}\n\t\t\t<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>\n\t\t\t\t<table className=\"text-sm w-full h-full text-nowrap\">\n\t\t\t\t\t<ContainersTableHead table={table} />\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{rows.length ? (\n\t\t\t\t\t\t\tvirtualRows.map((virtualRow) => {\n\t\t\t\t\t\t\t\tconst row = rows[virtualRow.index]\n\t\t\t\t\t\t\t\treturn <ContainerTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t<TableCell colSpan={colLength} className=\"h-37 text-center pointer-events-none\">\n\t\t\t\t\t\t\t\t\t{data ? (\n\t\t\t\t\t\t\t\t\t\t<Trans>No results.</Trans>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<LoaderCircleIcon className=\"animate-spin size-10 opacity-60 mx-auto\" />\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t\t<ContainerSheet sheetOpen={sheetOpen} setSheetOpen={setSheetOpen} activeContainer={activeContainer} />\n\t\t</div>\n\t)\n})\n\nasync function getLogsHtml(container: ContainerRecord): Promise<string> {\n\ttry {\n\t\tconst [{ highlighter }, logsHtml] = await Promise.all([\n\t\t\timport(\"@/lib/shiki\"),\n\t\t\tpb.send<{ logs: string }>(\"/api/beszel/containers/logs\", {\n\t\t\t\tsystem: container.system,\n\t\t\t\tcontainer: container.id,\n\t\t\t}),\n\t\t])\n\t\treturn logsHtml.logs ? highlighter.codeToHtml(logsHtml.logs, { lang: \"log\", theme: syntaxTheme }) : t`No results.`\n\t} catch (error) {\n\t\tconsole.error(error)\n\t\treturn \"\"\n\t}\n}\n\nasync function getInfoHtml(container: ContainerRecord): Promise<string> {\n\ttry {\n\t\tlet [{ highlighter }, { info }] = await Promise.all([\n\t\t\timport(\"@/lib/shiki\"),\n\t\t\tpb.send<{ info: string }>(\"/api/beszel/containers/info\", {\n\t\t\t\tsystem: container.system,\n\t\t\t\tcontainer: container.id,\n\t\t\t}),\n\t\t])\n\t\ttry {\n\t\t\tinfo = JSON.stringify(JSON.parse(info), null, 2)\n\t\t} catch (_) {}\n\t\treturn info ? highlighter.codeToHtml(info, { lang: \"json\", theme: syntaxTheme }) : t`No results.`\n\t} catch (error) {\n\t\tconsole.error(error)\n\t\treturn \"\"\n\t}\n}\n\nfunction ContainerSheet({\n\tsheetOpen,\n\tsetSheetOpen,\n\tactiveContainer,\n}: {\n\tsheetOpen: boolean\n\tsetSheetOpen: (open: boolean) => void\n\tactiveContainer: RefObject<ContainerRecord | null>\n}) {\n\tconst [logsDisplay, setLogsDisplay] = useState<string>(\"\")\n\tconst [infoDisplay, setInfoDisplay] = useState<string>(\"\")\n\tconst [logsFullscreenOpen, setLogsFullscreenOpen] = useState<boolean>(false)\n\tconst [infoFullscreenOpen, setInfoFullscreenOpen] = useState<boolean>(false)\n\tconst [isRefreshingLogs, setIsRefreshingLogs] = useState<boolean>(false)\n\tconst logsContainerRef = useRef<HTMLDivElement>(null)\n\n\tconst container = activeContainer.current\n\n\tfunction scrollLogsToBottom() {\n\t\tif (logsContainerRef.current) {\n\t\t\tlogsContainerRef.current.scrollTo({ top: logsContainerRef.current.scrollHeight })\n\t\t}\n\t}\n\n\tconst refreshLogs = async () => {\n\t\tif (!container) return\n\t\tsetIsRefreshingLogs(true)\n\t\tconst startTime = Date.now()\n\n\t\ttry {\n\t\t\tconst logsHtml = await getLogsHtml(container)\n\t\t\tsetLogsDisplay(logsHtml)\n\t\t\tsetTimeout(scrollLogsToBottom, 20)\n\t\t} catch (error) {\n\t\t\tconsole.error(error)\n\t\t} finally {\n\t\t\t// Ensure minimum spin duration of 800ms\n\t\t\tconst elapsed = Date.now() - startTime\n\t\t\tconst remaining = Math.max(0, 500 - elapsed)\n\t\t\tsetTimeout(() => {\n\t\t\t\tsetIsRefreshingLogs(false)\n\t\t\t}, remaining)\n\t\t}\n\t}\n\n\tuseEffect(() => {\n\t\tsetLogsDisplay(\"\")\n\t\tsetInfoDisplay(\"\")\n\t\tif (!container) return\n\t\t;(async () => {\n\t\t\tconst [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])\n\t\t\tsetLogsDisplay(logsHtml)\n\t\t\tsetInfoDisplay(infoHtml)\n\t\t\tsetTimeout(scrollLogsToBottom, 20)\n\t\t})()\n\t}, [container])\n\n\tif (!container) return null\n\n\treturn (\n\t\t<>\n\t\t\t<LogsFullscreenDialog\n\t\t\t\topen={logsFullscreenOpen}\n\t\t\t\tonOpenChange={setLogsFullscreenOpen}\n\t\t\t\tlogsDisplay={logsDisplay}\n\t\t\t\tcontainerName={container.name}\n\t\t\t\tonRefresh={refreshLogs}\n\t\t\t\tisRefreshing={isRefreshingLogs}\n\t\t\t/>\n\t\t\t<InfoFullscreenDialog\n\t\t\t\topen={infoFullscreenOpen}\n\t\t\t\tonOpenChange={setInfoFullscreenOpen}\n\t\t\t\tinfoDisplay={infoDisplay}\n\t\t\t\tcontainerName={container.name}\n\t\t\t/>\n\t\t\t<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>\n\t\t\t\t<SheetContent className=\"w-full sm:max-w-220 p-2\">\n\t\t\t\t\t<SheetHeader>\n\t\t\t\t\t\t<SheetTitle>{container.name}</SheetTitle>\n\t\t\t\t\t\t<SheetDescription className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n\t\t\t\t\t\t\t<Link className=\"hover:underline\" href={getPagePath($router, \"system\", { id: container.system })}>\n\t\t\t\t\t\t\t\t{$allSystemsById.get()[container.system]?.name ?? \"\"}\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t{container.status}\n\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t{container.image}\n\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t{container.id}\n\t\t\t\t\t\t\t{/* {container.ports && (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t\t\t{container.ports}\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t)} */}\n\t\t\t\t\t\t\t{/* <Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t{ContainerHealthLabels[container.health as ContainerHealth]} */}\n\t\t\t\t\t\t</SheetDescription>\n\t\t\t\t\t</SheetHeader>\n\t\t\t\t\t<div className=\"px-3 pb-3 -mt-4 flex flex-col gap-3 h-full items-start\">\n\t\t\t\t\t\t<div className=\"flex items-center w-full\">\n\t\t\t\t\t\t\t<h3>{t`Logs`}</h3>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\tonClick={refreshLogs}\n\t\t\t\t\t\t\t\tclassName=\"h-8 w-8 p-0 ms-auto\"\n\t\t\t\t\t\t\t\tdisabled={isRefreshingLogs}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<RefreshCwIcon\n\t\t\t\t\t\t\t\t\tclassName={`size-4 transition-transform duration-300 ${isRefreshingLogs ? \"animate-spin\" : \"\"}`}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"sm\" onClick={() => setLogsFullscreenOpen(true)} className=\"h-8 w-8 p-0\">\n\t\t\t\t\t\t\t\t<MaximizeIcon className=\"size-4\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tref={logsContainerRef}\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"max-h-[calc(50dvh-10rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm\",\n\t\t\t\t\t\t\t\t!logsDisplay && [\"animate-pulse\", \"h-full\"]\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div dangerouslySetInnerHTML={{ __html: logsDisplay }} />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex items-center w-full\">\n\t\t\t\t\t\t\t<h3>{t`Detail`}</h3>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\t\tonClick={() => setInfoFullscreenOpen(true)}\n\t\t\t\t\t\t\t\tclassName=\"h-8 w-8 p-0 ms-auto\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<MaximizeIcon className=\"size-4\" />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"grow h-[calc(50dvh-4rem)] w-full overflow-auto p-3 rounded-md bg-gh-dark text-white text-sm\",\n\t\t\t\t\t\t\t\t!infoDisplay && \"animate-pulse\"\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div dangerouslySetInnerHTML={{ __html: infoDisplay }} />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</SheetContent>\n\t\t\t</Sheet>\n\t\t</>\n\t)\n}\n\nfunction ContainersTableHead({ table }: { table: TableType<ContainerRecord> }) {\n\treturn (\n\t\t<TableHeader className=\"sticky top-0 z-50 w-full border-b-2\">\n\t\t\t<div className=\"absolute -top-2 left-0 w-full h-4 bg-table-header z-50\"></div>\n\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t<tr key={headerGroup.id}>\n\t\t\t\t\t{headerGroup.headers.map((header) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<TableHead className=\"px-2\" key={header.id} style={{ width: header.getSize() }}>\n\t\t\t\t\t\t\t\t{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t</tr>\n\t\t\t))}\n\t\t</TableHeader>\n\t)\n}\n\nconst ContainerTableRow = memo(function ContainerTableRow({\n\trow,\n\tvirtualRow,\n\topenSheet,\n}: {\n\trow: Row<ContainerRecord>\n\tvirtualRow: VirtualItem\n\topenSheet: (container: ContainerRecord) => void\n}) {\n\treturn (\n\t\t<TableRow\n\t\t\tdata-state={row.getIsSelected() && \"selected\"}\n\t\t\tclassName=\"cursor-pointer transition-opacity\"\n\t\t\tonClick={() => openSheet(row.original)}\n\t\t>\n\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t<TableCell\n\t\t\t\t\tkey={cell.id}\n\t\t\t\t\tclassName=\"py-0 ps-4.5\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: virtualRow.size,\n\t\t\t\t\t\twidth: cell.column.getSize(),\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t</TableCell>\n\t\t\t))}\n\t\t</TableRow>\n\t)\n})\n\nfunction LogsFullscreenDialog({\n\topen,\n\tonOpenChange,\n\tlogsDisplay,\n\tcontainerName,\n\tonRefresh,\n\tisRefreshing,\n}: {\n\topen: boolean\n\tonOpenChange: (open: boolean) => void\n\tlogsDisplay: string\n\tcontainerName: string\n\tonRefresh: () => void | Promise<void>\n\tisRefreshing: boolean\n}) {\n\tconst outerContainerRef = useRef<HTMLDivElement>(null)\n\n\tuseEffect(() => {\n\t\tif (open && logsDisplay) {\n\t\t\t// Scroll the outer container to bottom\n\t\t\tconst scrollToBottom = () => {\n\t\t\t\tif (outerContainerRef.current) {\n\t\t\t\t\touterContainerRef.current.scrollTop = outerContainerRef.current.scrollHeight\n\t\t\t\t}\n\t\t\t}\n\t\t\tsetTimeout(scrollToBottom, 50)\n\t\t}\n\t}, [open, logsDisplay])\n\n\treturn (\n\t\t<Dialog open={open} onOpenChange={onOpenChange}>\n\t\t\t<DialogContent className=\"w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white\">\n\t\t\t\t<DialogTitle className=\"sr-only\">{containerName} logs</DialogTitle>\n\t\t\t\t<div ref={outerContainerRef} className=\"h-full overflow-auto\">\n\t\t\t\t\t<div className=\"h-full w-full px-3 leading-relaxed rounded-md bg-gh-dark text-sm\">\n\t\t\t\t\t\t<div className=\"py-3\" dangerouslySetInnerHTML={{ __html: logsDisplay }} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<button\n\t\t\t\t\tonClick={onRefresh}\n\t\t\t\t\tclassName=\"absolute top-3 right-11 opacity-60 hover:opacity-100 p-1\"\n\t\t\t\t\tdisabled={isRefreshing}\n\t\t\t\t\ttitle={t`Refresh`}\n\t\t\t\t\taria-label={t`Refresh`}\n\t\t\t\t>\n\t\t\t\t\t<RefreshCwIcon className={`size-4 transition-transform duration-300 ${isRefreshing ? \"animate-spin\" : \"\"}`} />\n\t\t\t\t</button>\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n\nfunction InfoFullscreenDialog({\n\topen,\n\tonOpenChange,\n\tinfoDisplay,\n\tcontainerName,\n}: {\n\topen: boolean\n\tonOpenChange: (open: boolean) => void\n\tinfoDisplay: string\n\tcontainerName: string\n}) {\n\treturn (\n\t\t<Dialog open={open} onOpenChange={onOpenChange}>\n\t\t\t<DialogContent className=\"w-[calc(100vw-20px)] h-[calc(100dvh-20px)] max-w-none p-0 bg-gh-dark border-0 text-white\">\n\t\t\t\t<DialogTitle className=\"sr-only\">{containerName} info</DialogTitle>\n\t\t\t\t<div className=\"flex-1 overflow-auto\">\n\t\t\t\t\t<div className=\"h-full w-full overflow-auto p-3 rounded-md bg-gh-dark text-sm leading-relaxed\">\n\t\t\t\t\t\t<div dangerouslySetInnerHTML={{ __html: infoDisplay }} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/copy-to-clipboard.tsx",
    "content": "import { Trans } from \"@lingui/react/macro\"\nimport { useEffect, useMemo, useRef } from \"react\"\nimport { $copyContent } from \"@/lib/stores\"\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./ui/dialog\"\nimport { Textarea } from \"./ui/textarea\"\n\nexport default function CopyToClipboard({ content }: { content: string }) {\n\treturn (\n\t\t<Dialog defaultOpen={true}>\n\t\t\t<DialogContent className=\"w-[90%] rounded-lg md:pt-4\" style={{ maxWidth: 530 }}>\n\t\t\t\t<DialogHeader>\n\t\t\t\t\t<DialogTitle>\n\t\t\t\t\t\t<Trans>Copy text</Trans>\n\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t<DialogDescription className=\"hidden xs:block\">\n\t\t\t\t\t\t<Trans>Automatic copy requires a secure context.</Trans>\n\t\t\t\t\t</DialogDescription>\n\t\t\t\t</DialogHeader>\n\t\t\t\t<CopyTextarea content={content} />\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n\nfunction CopyTextarea({ content }: { content: string }) {\n\tconst textareaRef = useRef<HTMLTextAreaElement>(null)\n\n\tconst rows = useMemo(() => {\n\t\treturn content.split(\"\\n\").length\n\t}, [content])\n\n\tuseEffect(() => {\n\t\tif (textareaRef.current) {\n\t\t\ttextareaRef.current.select()\n\t\t}\n\t}, [textareaRef])\n\n\tuseEffect(() => {\n\t\treturn () => $copyContent.set(\"\")\n\t}, [])\n\n\treturn (\n\t\t<Textarea\n\t\t\tclassName=\"font-mono overflow-hidden whitespace-pre\"\n\t\t\trows={rows}\n\t\t\tvalue={content}\n\t\t\treadOnly\n\t\t\tref={textareaRef}\n\t\t/>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/footer-repo-link.tsx",
    "content": "import { GithubIcon } from \"lucide-react\"\nimport { Separator } from \"./ui/separator\"\n\nexport function FooterRepoLink() {\n\treturn (\n\t\t<div className=\"flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80\">\n\t\t\t<a\n\t\t\t\thref=\"https://github.com/henrygd/beszel\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclassName=\"flex items-center gap-0.5 text-muted-foreground hover:text-foreground duration-75\"\n\t\t\t\trel=\"noopener\"\n\t\t\t>\n\t\t\t\t<GithubIcon className=\"h-3 w-3\" /> GitHub\n\t\t\t</a>\n\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t<a\n\t\t\t\thref=\"https://github.com/henrygd/beszel/releases\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tclassName=\"text-muted-foreground hover:text-foreground duration-75\"\n\t\t\t\trel=\"noopener\"\n\t\t\t>\n\t\t\t\tBeszel {globalThis.BESZEL.HUB_VERSION}\n\t\t\t</a>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/install-dropdowns.tsx",
    "content": "import { i18n } from \"@lingui/core\"\nimport { memo } from \"react\"\nimport { copyToClipboard, getHubURL } from \"@/lib/utils\"\nimport { DropdownMenuContent, DropdownMenuItem } from \"./ui/dropdown-menu\"\n\n// const isbeta = beszel.hub_version.includes(\"beta\")\n// const imagetag = isbeta ? \":edge\" : \"\"\n\n/**\n * Get the URL of the script to install the agent.\n * @param path - The path to the script (e.g. \"/brew\").\n * @returns The URL for the script.\n */\nconst getScriptUrl = (path: string = \"\") => {\n\treturn `https://get.beszel.dev${path}`\n\t// no beta for now\n\t// const url = new URL(\"https://get.beszel.dev\")\n\t// url.pathname = path\n\t// if (isBeta) {\n\t// \turl.searchParams.set(\"beta\", \"1\")\n\t// }\n\t// return url.toString()\n}\n\nexport function copyDockerCompose(port = \"45876\", publicKey: string, token: string) {\n\tcopyToClipboard(`services:\n  beszel-agent:\n    image: henrygd/beszel-agent\n    container_name: beszel-agent\n    restart: unless-stopped\n    network_mode: host\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n      - ./beszel_agent_data:/var/lib/beszel-agent\n      # monitor other disks / partitions by mounting a folder in /extra-filesystems\n      # - /mnt/disk/.beszel:/extra-filesystems/sda1:ro\n    environment:\n      LISTEN: ${port}\n      KEY: '${publicKey}'\n      TOKEN: ${token}\n      HUB_URL: ${getHubURL()}`)\n}\n\nexport function copyDockerRun(port = \"45876\", publicKey: string, token: string) {\n\tcopyToClipboard(\n\t\t`docker run -d --name beszel-agent --network host --restart unless-stopped -v /var/run/docker.sock:/var/run/docker.sock:ro -v beszel_agent_data:/var/lib/beszel-agent -e KEY=\"${publicKey}\" -e LISTEN=${port} -e TOKEN=\"${token}\" -e HUB_URL=\"${getHubURL()}\" henrygd/beszel-agent`\n\t)\n}\n\nexport function copyLinuxCommand(port = \"45876\", publicKey: string, token: string, brew = false) {\n\tlet cmd = `curl -sL ${getScriptUrl(\n\t\tbrew ? \"/brew\" : \"\"\n\t)} -o /tmp/install-agent.sh && chmod +x /tmp/install-agent.sh && /tmp/install-agent.sh -p ${port} -k \"${publicKey}\" -t \"${token}\" -url \"${getHubURL()}\"`\n\t// brew script does not support --china-mirrors\n\tif (!brew && (i18n.locale + navigator.language).includes(\"zh-CN\")) {\n\t\tcmd += ` --china-mirrors`\n\t}\n\tcopyToClipboard(cmd)\n}\n\nexport function copyWindowsCommand(port = \"45876\", publicKey: string, token: string) {\n\tcopyToClipboard(\n\t\t`& iwr -useb ${getScriptUrl()} -OutFile \"$env:TEMP\\\\install-agent.ps1\"; & Powershell -ExecutionPolicy Bypass -File \"$env:TEMP\\\\install-agent.ps1\" -Key \"${publicKey}\" -Port ${port} -Token \"${token}\" -Url \"${getHubURL()}\"`\n\t)\n}\n\nexport interface DropdownItem {\n\ttext: string\n\tonClick?: () => void\n\turl?: string\n\ticons?: React.ComponentType<React.SVGProps<SVGSVGElement>>[]\n}\n\nexport const InstallDropdown = memo(({ items }: { items: DropdownItem[] }) => {\n\treturn (\n\t\t<DropdownMenuContent align=\"end\">\n\t\t\t{items.map((item, index) => {\n\t\t\t\tconst className = \"cursor-pointer flex items-center gap-1.5\"\n\t\t\t\treturn item.url ? (\n\t\t\t\t\t<DropdownMenuItem key={index} asChild>\n\t\t\t\t\t\t<a href={item.url} className={className} target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t\t{item.text}{\" \"}\n\t\t\t\t\t\t\t{item.icons?.map((Icon, iconIndex) => (\n\t\t\t\t\t\t\t\t<Icon key={iconIndex} className=\"size-4\" />\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t) : (\n\t\t\t\t\t<DropdownMenuItem key={index} onClick={item.onClick} className={className}>\n\t\t\t\t\t\t{item.text}{\" \"}\n\t\t\t\t\t\t{item.icons?.map((Icon, iconIndex) => (\n\t\t\t\t\t\t\t<Icon key={iconIndex} className=\"size-4\" />\n\t\t\t\t\t\t))}\n\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t)\n\t\t\t})}\n\t\t</DropdownMenuContent>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/lang-toggle.tsx",
    "content": "import { Trans, useLingui } from \"@lingui/react/macro\"\nimport { LanguagesIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from \"@/components/ui/dropdown-menu\"\nimport { dynamicActivate } from \"@/lib/i18n\"\nimport languages from \"@/lib/languages\"\nimport { cn } from \"@/lib/utils\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./ui/tooltip\"\n\nexport function LangToggle() {\n\tconst { i18n } = useLingui()\n\n\tconst LangTrans = <Trans>Language</Trans>\n\n\treturn (\n\t\t<DropdownMenu>\n\t\t\t<DropdownMenuTrigger>\n\t\t\t\t<Tooltip>\n\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t<Button variant={\"ghost\"} size=\"icon\" className=\"hidden sm:flex\">\n\t\t\t\t\t\t\t<LanguagesIcon className=\"absolute h-[1.2rem] w-[1.2rem] light:opacity-85\" />\n\t\t\t\t\t\t\t<span className=\"sr-only\">{LangTrans}</span>\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t<TooltipContent>{LangTrans}</TooltipContent>\n\t\t\t\t</Tooltip>\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent className=\"grid grid-cols-3\">\n\t\t\t\t{languages.map(([lang, label, e]) => (\n\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\tkey={lang}\n\t\t\t\t\t\tclassName={cn(\"px-2.5 flex gap-2.5 cursor-pointer\", lang === i18n.locale && \"bg-accent/70 font-medium\")}\n\t\t\t\t\t\tonClick={() => dynamicActivate(lang)}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t{e || <code className=\"font-mono bg-muted text-[.65em] w-5 h-4 grid place-items-center\">{lang}</code>}\n\t\t\t\t\t\t</span>{\" \"}\n\t\t\t\t\t\t{label}\n\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t))}\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/login/auth-form.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { KeyIcon, LoaderCircle, LockIcon, LogInIcon, MailIcon } from \"lucide-react\"\nimport type { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from \"pocketbase\"\nimport { useCallback, useEffect, useState } from \"react\"\nimport * as v from \"valibot\"\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { pb } from \"@/lib/api\"\nimport { $authenticated } from \"@/lib/stores\"\nimport { cn } from \"@/lib/utils\"\nimport { $router, Link, prependBasePath } from \"../router\"\nimport { toast } from \"../ui/use-toast\"\nimport { OtpInputForm } from \"./otp-forms\"\n\nconst honeypot = v.literal(\"\")\nconst emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))\nconst passwordSchema = v.pipe(\n\tv.string(),\n\tv.minLength(8, t`Password must be at least 8 characters.`),\n\tv.maxBytes(72, t`Password must be less than 72 bytes.`)\n)\n\nconst LoginSchema = v.looseObject({\n\twebsite: honeypot,\n\temail: emailSchema,\n\tpassword: passwordSchema,\n})\n\nconst RegisterSchema = v.looseObject({\n\twebsite: honeypot,\n\temail: emailSchema,\n\tpassword: passwordSchema,\n\tpasswordConfirm: passwordSchema,\n})\n\nexport const showLoginFaliedToast = (description?: string) => {\n\tdescription ||= t`Please check your credentials and try again`\n\ttoast({\n\t\ttitle: t`Login attempt failed`,\n\t\tdescription,\n\t\tvariant: \"destructive\",\n\t})\n}\n\nconst getAuthProviderIcon = (provider: AuthProviderInfo) => {\n\tlet { name } = provider\n\tif (name.startsWith(\"oidc\")) {\n\t\tname = \"oidc\"\n\t}\n\treturn prependBasePath(`/_/images/oauth2/${name}.svg`)\n}\n\nexport function UserAuthForm({\n\tclassName,\n\tisFirstRun,\n\tauthMethods,\n\t...props\n}: {\n\tclassName?: string\n\tisFirstRun: boolean\n\tauthMethods: AuthMethodsList\n}) {\n\tconst [isLoading, setIsLoading] = useState<boolean>(false)\n\tconst [isOauthLoading, setIsOauthLoading] = useState<boolean>(false)\n\tconst [errors, setErrors] = useState<Record<string, string | undefined>>({})\n\tconst [mfaId, setMfaId] = useState<string | undefined>()\n\tconst [otpId, setOtpId] = useState<string | undefined>()\n\n\tconst handleSubmit = useCallback(\n\t\tasync (e: React.FormEvent<HTMLFormElement>) => {\n\t\t\te.preventDefault()\n\t\t\tsetIsLoading(true)\n\t\t\t// store email for later use if mfa is enabled\n\t\t\tlet email = \"\"\n\t\t\ttry {\n\t\t\t\tconst formData = new FormData(e.target as HTMLFormElement)\n\t\t\t\tconst data = Object.fromEntries(formData) as Record<string, any>\n\t\t\t\tconst Schema = isFirstRun ? RegisterSchema : LoginSchema\n\t\t\t\tconst result = v.safeParse(Schema, data)\n\t\t\t\tif (!result.success) {\n\t\t\t\t\tconsole.log(result)\n\t\t\t\t\tconst errors = {}\n\t\t\t\t\tfor (const issue of result.issues) {\n\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\terrors[issue.path[0].key] = issue.message\n\t\t\t\t\t}\n\t\t\t\t\tsetErrors(errors)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst { password, passwordConfirm } = result.output\n\t\t\t\temail = result.output.email\n\t\t\t\tif (isFirstRun) {\n\t\t\t\t\t// check that passwords match\n\t\t\t\t\tif (password !== passwordConfirm) {\n\t\t\t\t\t\tconst msg = \"Passwords do not match\"\n\t\t\t\t\t\tsetErrors({ passwordConfirm: msg })\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tawait pb.send(\"/api/beszel/create-user\", {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\tbody: JSON.stringify({ email, password }),\n\t\t\t\t\t})\n\t\t\t\t\tawait pb.collection(\"users\").authWithPassword(email, password)\n\t\t\t\t} else {\n\t\t\t\t\tawait pb.collection(\"users\").authWithPassword(email, password)\n\t\t\t\t}\n\t\t\t\t$authenticated.set(true)\n\t\t\t} catch (err: any) {\n\t\t\t\tconst mfaId = err?.response?.mfaId\n\t\t\t\tif (!mfaId) {\n\t\t\t\t\tshowLoginFaliedToast()\n\t\t\t\t\tthrow err\n\t\t\t\t}\n\t\t\t\tsetMfaId(mfaId)\n\t\t\t\ttry {\n\t\t\t\t\tconst { otpId } = await pb.collection(\"users\").requestOTP(email)\n\t\t\t\t\tsetOtpId(otpId)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.log({ err })\n\t\t\t\t\tshowLoginFaliedToast()\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tsetIsLoading(false)\n\t\t\t}\n\t\t},\n\t\t[isFirstRun]\n\t)\n\n\tif (!authMethods) {\n\t\treturn null\n\t}\n\n\tconst authProviders = authMethods.oauth2.providers ?? []\n\tconst oauthEnabled = authMethods.oauth2.enabled && authProviders.length > 0\n\tconst passwordEnabled = authMethods.password.enabled\n\tconst otpEnabled = authMethods.otp.enabled\n\tconst mfaEnabled = authMethods.mfa.enabled\n\n\tfunction loginWithOauth(provider: AuthProviderInfo, forcePopup = false) {\n\t\tsetIsOauthLoading(true)\n\t\tconst oAuthOpts: OAuth2AuthConfig = {\n\t\t\tprovider: provider.name,\n\t\t}\n\t\t// https://github.com/pocketbase/pocketbase/discussions/2429#discussioncomment-5943061\n\t\tif (forcePopup || navigator.userAgent.match(/iPhone|iPad|iPod/i)) {\n\t\t\tconst authWindow = window.open()\n\t\t\tif (!authWindow) {\n\t\t\t\tsetIsOauthLoading(false)\n\t\t\t\ttoast({\n\t\t\t\t\ttitle: t`Error`,\n\t\t\t\t\tdescription: t`Please enable pop-ups for this site`,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\toAuthOpts.urlCallback = (url) => {\n\t\t\t\tauthWindow.location.href = url\n\t\t\t}\n\t\t}\n\t\tpb.collection(\"users\")\n\t\t\t.authWithOAuth2(oAuthOpts)\n\t\t\t.then(() => {\n\t\t\t\t$authenticated.set(pb.authStore.isValid)\n\t\t\t})\n\t\t\t.catch(showLoginFaliedToast)\n\t\t\t.finally(() => {\n\t\t\t\tsetIsOauthLoading(false)\n\t\t\t})\n\t}\n\n\tuseEffect(() => {\n\t\t// auto login if password disabled and only one auth provider\n\t\tif (!passwordEnabled && authProviders.length === 1 && !sessionStorage.getItem(\"lo\")) {\n\t\t\t// Add a small timeout to ensure browser is ready to handle popups\n\t\t\tsetTimeout(() => {\n\t\t\t\tloginWithOauth(authProviders[0], true)\n\t\t\t}, 300)\n\t\t}\n\t}, [])\n\n\tif (otpId && mfaId) {\n\t\treturn <OtpInputForm otpId={otpId} mfaId={mfaId} />\n\t}\n\n\treturn (\n\t\t<div className={cn(\"grid gap-6\", className)} {...props}>\n\t\t\t{passwordEnabled && (\n\t\t\t\t<>\n\t\t\t\t\t<form onSubmit={handleSubmit} onChange={() => setErrors({})}>\n\t\t\t\t\t\t<div className=\"grid gap-2.5\">\n\t\t\t\t\t\t\t<div className=\"grid gap-1 relative\">\n\t\t\t\t\t\t\t\t<MailIcon className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t<Label className=\"sr-only\" htmlFor=\"email\">\n\t\t\t\t\t\t\t\t\t<Trans>Email</Trans>\n\t\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t\t\t\t\tname=\"email\"\n\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\tplaceholder=\"name@example.com\"\n\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\tautoCapitalize=\"none\"\n\t\t\t\t\t\t\t\t\tautoComplete=\"email\"\n\t\t\t\t\t\t\t\t\tautoCorrect=\"off\"\n\t\t\t\t\t\t\t\t\tdisabled={isLoading || isOauthLoading}\n\t\t\t\t\t\t\t\t\tclassName={cn(\"ps-9\", errors?.email && \"border-red-500\")}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{errors?.email && <p className=\"px-1 text-xs text-red-600\">{errors.email}</p>}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"grid gap-1 relative\">\n\t\t\t\t\t\t\t\t<LockIcon className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t<Label className=\"sr-only\" htmlFor=\"pass\">\n\t\t\t\t\t\t\t\t\t<Trans>Password</Trans>\n\t\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\tid=\"pass\"\n\t\t\t\t\t\t\t\t\tname=\"password\"\n\t\t\t\t\t\t\t\t\tplaceholder={t`Password`}\n\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\tautoComplete=\"current-password\"\n\t\t\t\t\t\t\t\t\tdisabled={isLoading || isOauthLoading}\n\t\t\t\t\t\t\t\t\tclassName={cn(\"ps-9\", errors?.password && \"border-red-500\")}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{errors?.password && <p className=\"px-1 text-xs text-red-600\">{errors.password}</p>}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{isFirstRun && (\n\t\t\t\t\t\t\t\t<div className=\"grid gap-1 relative\">\n\t\t\t\t\t\t\t\t\t<LockIcon className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t<Label className=\"sr-only\" htmlFor=\"pass2\">\n\t\t\t\t\t\t\t\t\t\t<Trans>Confirm password</Trans>\n\t\t\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\tid=\"pass2\"\n\t\t\t\t\t\t\t\t\t\tname=\"passwordConfirm\"\n\t\t\t\t\t\t\t\t\t\tplaceholder={t`Confirm password`}\n\t\t\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\t\t\ttype=\"password\"\n\t\t\t\t\t\t\t\t\t\tautoComplete=\"current-password\"\n\t\t\t\t\t\t\t\t\t\tdisabled={isLoading || isOauthLoading}\n\t\t\t\t\t\t\t\t\t\tclassName={cn(\"ps-9\", errors?.password && \"border-red-500\")}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t{errors?.passwordConfirm && <p className=\"px-1 text-xs text-red-600\">{errors.passwordConfirm}</p>}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<div className=\"sr-only\">\n\t\t\t\t\t\t\t\t{/* honeypot */}\n\t\t\t\t\t\t\t\t<label htmlFor=\"website\"></label>\n\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\tid=\"website\"\n\t\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\t\tname=\"website\"\n\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\t\tdata-1p-ignore\n\t\t\t\t\t\t\t\t\tdata-lpignore=\"true\"\n\t\t\t\t\t\t\t\t\tdata-bwignore\n\t\t\t\t\t\t\t\t\tdata-form-type=\"other\"\n\t\t\t\t\t\t\t\t\tdata-protonpass-ignore\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<button className={cn(buttonVariants())} disabled={isLoading}>\n\t\t\t\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t\t\t\t<LoaderCircle className=\"me-2 h-4 w-4 animate-spin\" />\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<LogInIcon className=\"me-2 h-4 w-4\" />\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t{isFirstRun ? t`Create account` : t`Sign in`}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</form>\n\t\t\t\t\t{(isFirstRun || oauthEnabled || (otpEnabled && !mfaEnabled)) && (\n\t\t\t\t\t\t// only show 'continue with' during onboarding or if we have auth providers\n\t\t\t\t\t\t<div className=\"relative\">\n\t\t\t\t\t\t\t<div className=\"absolute inset-0 flex items-center\">\n\t\t\t\t\t\t\t\t<span className=\"w-full border-t\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div className=\"relative flex justify-center text-xs uppercase\">\n\t\t\t\t\t\t\t\t<span className=\"bg-background px-2 text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t<Trans>Or continue with</Trans>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t)}\n\t\t\t{/* hide OTP button if MFA is enabled (it will be used as MFA) */}\n\t\t\t{otpEnabled && !mfaEnabled && (\n\t\t\t\t<div className=\"grid gap-2 -mt-1\">\n\t\t\t\t\t<Link href=\"/request-otp\" type=\"button\" className={cn(buttonVariants({ variant: \"outline\" }), \"flex gap-2\")}>\n\t\t\t\t\t\t<KeyIcon className=\"size-4\" />\n\t\t\t\t\t\t<Trans>One-time password</Trans>\n\t\t\t\t\t</Link>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{oauthEnabled && (\n\t\t\t\t<div className=\"grid gap-2 -mt-1\">\n\t\t\t\t\t{authMethods.oauth2.providers.map((provider) => (\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={provider.name}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"outline\" }), {\n\t\t\t\t\t\t\t\t\"justify-self-center\": !passwordEnabled,\n\t\t\t\t\t\t\t\t\"px-5\": !passwordEnabled,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\tonClick={() => loginWithOauth(provider)}\n\t\t\t\t\t\t\tdisabled={isLoading || isOauthLoading}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{isOauthLoading ? (\n\t\t\t\t\t\t\t\t<LoaderCircle className=\"me-2 h-4 w-4 animate-spin\" />\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tclassName=\"me-2 h-4 w-4 dark:brightness-0 dark:invert\"\n\t\t\t\t\t\t\t\t\tsrc={getAuthProviderIcon(provider)}\n\t\t\t\t\t\t\t\t\talt=\"\"\n\t\t\t\t\t\t\t\t\t// onError={(e) => {\n\t\t\t\t\t\t\t\t\t// \te.currentTarget.src = \"/static/lock.svg\"\n\t\t\t\t\t\t\t\t\t// }}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<span className=\"translate-y-px\">{provider.displayName}</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t\t{!oauthEnabled && isFirstRun && (\n\t\t\t\t// only show GitHub button / dialog during onboarding\n\t\t\t\t<Dialog>\n\t\t\t\t\t<DialogTrigger asChild>\n\t\t\t\t\t\t<button type=\"button\" className={cn(buttonVariants({ variant: \"outline\" }))}>\n\t\t\t\t\t\t\t<img className=\"me-2 h-4 w-4 dark:invert\" src={prependBasePath(\"/_/images/oauth2/github.svg\")} alt=\"\" />\n\t\t\t\t\t\t\t<span className=\"translate-y-px\">GitHub</span>\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</DialogTrigger>\n\t\t\t\t\t<DialogContent style={{ maxWidth: 440, width: \"90%\" }}>\n\t\t\t\t\t\t<DialogHeader>\n\t\t\t\t\t\t\t<DialogTitle>\n\t\t\t\t\t\t\t\t<Trans>OAuth 2 / OIDC support</Trans>\n\t\t\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t\t</DialogHeader>\n\t\t\t\t\t\t<div className=\"text-primary/70 text-[0.95em] contents\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<Trans>Beszel supports OpenID Connect and many OAuth2 authentication providers.</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tPlease see{\" \"}\n\t\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\t\thref=\"https://beszel.dev/guide/oauth\"\n\t\t\t\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"link\" }), \"p-0 h-auto\")}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tthe documentation\n\t\t\t\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\t\t\t\tfor instructions.\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</DialogContent>\n\t\t\t\t</Dialog>\n\t\t\t)}\n\t\t\t{passwordEnabled && !isFirstRun && (\n\t\t\t\t<Link\n\t\t\t\t\thref={getPagePath($router, \"forgot_password\")}\n\t\t\t\t\tclassName=\"text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity\"\n\t\t\t\t>\n\t\t\t\t\t<Trans>Forgot password?</Trans>\n\t\t\t\t</Link>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/login/forgot-pass-form.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { LoaderCircle, MailIcon, SendHorizonalIcon } from \"lucide-react\"\nimport { useCallback, useState } from \"react\"\nimport { pb } from \"@/lib/api\"\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"../ui/button\"\nimport { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from \"../ui/dialog\"\nimport { Input } from \"../ui/input\"\nimport { Label } from \"../ui/label\"\nimport { toast } from \"../ui/use-toast\"\n\nconst showLoginFaliedToast = () => {\n\ttoast({\n\t\ttitle: t`Login attempt failed`,\n\t\tdescription: t`Please check your credentials and try again`,\n\t\tvariant: \"destructive\",\n\t})\n}\n\nexport default function ForgotPassword() {\n\tconst [isLoading, setIsLoading] = useState<boolean>(false)\n\tconst [email, setEmail] = useState(\"\")\n\n\tconst handleSubmit = useCallback(\n\t\tasync (e: React.FormEvent<HTMLFormElement>) => {\n\t\t\te.preventDefault()\n\t\t\tsetIsLoading(true)\n\t\t\ttry {\n\t\t\t\t// console.log(email)\n\t\t\t\tawait pb.collection(\"users\").requestPasswordReset(email)\n\t\t\t\ttoast({\n\t\t\t\t\ttitle: t`Password reset request received`,\n\t\t\t\t\tdescription: t`Check ${email} for a reset link.`,\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tshowLoginFaliedToast()\n\t\t\t} finally {\n\t\t\t\tsetIsLoading(false)\n\t\t\t\tsetEmail(\"\")\n\t\t\t}\n\t\t},\n\t\t[email]\n\t)\n\n\treturn (\n\t\t<>\n\t\t\t<form onSubmit={handleSubmit}>\n\t\t\t\t<div className=\"grid gap-3\">\n\t\t\t\t\t<div className=\"grid gap-1 relative\">\n\t\t\t\t\t\t<MailIcon className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n\t\t\t\t\t\t<Label className=\"sr-only\" htmlFor=\"email\">\n\t\t\t\t\t\t\t<Trans>Email</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tvalue={email}\n\t\t\t\t\t\t\tonChange={(e) => setEmail(e.target.value)}\n\t\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t\t\tname=\"email\"\n\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\tplaceholder=\"name@example.com\"\n\t\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\t\tautoCapitalize=\"none\"\n\t\t\t\t\t\t\tautoComplete=\"email\"\n\t\t\t\t\t\t\tautoCorrect=\"off\"\n\t\t\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t\t\t\tclassName=\"ps-9\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<button className={cn(buttonVariants())} disabled={isLoading}>\n\t\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t\t<LoaderCircle className=\"me-2 h-4 w-4 animate-spin\" />\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<SendHorizonalIcon className=\"me-2 h-4 w-4\" />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<Trans>Reset Password</Trans>\n\t\t\t\t\t</button>\n\t\t\t\t</div>\n\t\t\t</form>\n\t\t\t<Dialog>\n\t\t\t\t<DialogTrigger asChild>\n\t\t\t\t\t<button className=\"text-sm mx-auto hover:text-brand underline underline-offset-4 opacity-70 hover:opacity-100 transition-opacity\">\n\t\t\t\t\t\t<Trans>Command line instructions</Trans>\n\t\t\t\t\t</button>\n\t\t\t\t</DialogTrigger>\n\t\t\t\t<DialogContent className=\"max-w-[41em]\">\n\t\t\t\t\t<DialogHeader>\n\t\t\t\t\t\t<DialogTitle>\n\t\t\t\t\t\t\t<Trans>Command line instructions</Trans>\n\t\t\t\t\t\t</DialogTitle>\n\t\t\t\t\t</DialogHeader>\n\t\t\t\t\t<p className=\"text-primary/70 text-[0.95em] leading-relaxed\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tIf you've lost the password to your admin account, you may reset it using the following command.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t\t<p className=\"text-primary/70 text-[0.95em] leading-relaxed\">\n\t\t\t\t\t\t<Trans>Then log into the backend and reset your user account password in the users table.</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t\t<code className=\"bg-muted rounded-sm py-0.5 px-2.5 me-auto text-sm\">\n\t\t\t\t\t\t./beszel superuser upsert user@example.com password\n\t\t\t\t\t</code>\n\t\t\t\t\t<code className=\"bg-muted rounded-sm py-0.5 px-2.5 me-auto text-sm\">\n\t\t\t\t\t\tdocker exec beszel /beszel superuser upsert name@example.com password\n\t\t\t\t\t</code>\n\t\t\t\t</DialogContent>\n\t\t\t</Dialog>\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/login/login.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport type { AuthMethodsList } from \"pocketbase\"\nimport { useEffect, useMemo, useState } from \"react\"\nimport { UserAuthForm } from \"@/components/login/auth-form\"\nimport { pb } from \"@/lib/api\"\nimport { Logo } from \"../logo\"\nimport { ModeToggle } from \"../mode-toggle\"\nimport { $router } from \"../router\"\nimport { useTheme } from \"../theme-provider\"\nimport ForgotPassword from \"./forgot-pass-form\"\nimport { OtpRequestForm } from \"./otp-forms\"\n\nexport default function () {\n\tconst page = useStore($router)\n\tconst [isFirstRun, setFirstRun] = useState(false)\n\tconst [authMethods, setAuthMethods] = useState<AuthMethodsList>()\n\tconst { theme } = useTheme()\n\n\tuseEffect(() => {\n\t\tdocument.title = t`Login` + \" / Beszel\"\n\n\t\tpb.send(\"/api/beszel/first-run\", {}).then(({ firstRun }) => {\n\t\t\tsetFirstRun(firstRun)\n\t\t})\n\t}, [])\n\n\tuseEffect(() => {\n\t\tpb.collection(\"users\")\n\t\t\t.listAuthMethods()\n\t\t\t.then((methods) => {\n\t\t\t\tsetAuthMethods(methods)\n\t\t\t})\n\t}, [])\n\n\tconst subtitle = useMemo(() => {\n\t\tif (isFirstRun) {\n\t\t\treturn t`Please create an admin account`\n\t\t} else if (page?.route === \"forgot_password\") {\n\t\t\treturn t`Enter email address to reset password`\n\t\t} else if (page?.route === \"request_otp\") {\n\t\t\treturn t`Request a one-time password`\n\t\t} else {\n\t\t\treturn t`Please sign in to your account`\n\t\t}\n\t}, [isFirstRun, page])\n\n\tif (!authMethods) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div className=\"min-h-svh grid items-center py-12\">\n\t\t\t<div\n\t\t\t\tclassName=\"grid gap-5 w-full px-4 mx-auto\"\n\t\t\t\t// @ts-expect-error\n\t\t\t\tstyle={{ maxWidth: \"21.5em\", \"--border\": theme == \"light\" ? \"hsl(30, 8%, 70%)\" : \"hsl(220, 3%, 25%)\" }}\n\t\t\t>\n\t\t\t\t<div className=\"absolute top-3 right-3\">\n\t\t\t\t\t<ModeToggle />\n\t\t\t\t</div>\n\t\t\t\t<div className=\"text-center\">\n\t\t\t\t\t<h1 className=\"mb-3\">\n\t\t\t\t\t\t<Logo className=\"h-7 fill-foreground mx-auto\" />\n\t\t\t\t\t\t<span className=\"sr-only\">Beszel</span>\n\t\t\t\t\t</h1>\n\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">{subtitle}</p>\n\t\t\t\t</div>\n\t\t\t\t{page?.route === \"forgot_password\" ? (\n\t\t\t\t\t<ForgotPassword />\n\t\t\t\t) : page?.route === \"request_otp\" ? (\n\t\t\t\t\t<OtpRequestForm />\n\t\t\t\t) : (\n\t\t\t\t\t<UserAuthForm isFirstRun={isFirstRun} authMethods={authMethods} />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/login/otp-forms.tsx",
    "content": "import { Trans } from \"@lingui/react/macro\"\nimport { LoaderCircle, MailIcon, SendHorizonalIcon } from \"lucide-react\"\nimport { useCallback, useState } from \"react\"\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from \"@/components/ui/otp\"\nimport { pb } from \"@/lib/api\"\nimport { $authenticated } from \"@/lib/stores\"\nimport { cn } from \"@/lib/utils\"\nimport { $router } from \"../router\"\nimport { buttonVariants } from \"../ui/button\"\nimport { Input } from \"../ui/input\"\nimport { Label } from \"../ui/label\"\nimport { showLoginFaliedToast } from \"./auth-form\"\n\nexport function OtpInputForm({ otpId, mfaId }: { otpId: string; mfaId: string }) {\n\tconst [value, setValue] = useState(\"\")\n\n\tif (value.length === 6) {\n\t\tpb.collection(\"users\")\n\t\t\t.authWithOTP(otpId, value, { mfaId })\n\t\t\t.then(() => {\n\t\t\t\t$router.open(\"/\")\n\t\t\t\t$authenticated.set(true)\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tshowLoginFaliedToast(err.message)\n\t\t\t})\n\t}\n\n\treturn (\n\t\t<div className=\"grid gap-3 items-center justify-center\">\n\t\t\t<InputOTP maxLength={6} value={value} onChange={setValue} autoFocus>\n\t\t\t\t<InputOTPGroup>\n\t\t\t\t\t{Array.from({ length: 6 }).map((_, i) => (\n\t\t\t\t\t\t<InputOTPSlot key={i} index={i} />\n\t\t\t\t\t))}\n\t\t\t\t</InputOTPGroup>\n\t\t\t</InputOTP>\n\t\t\t<div className=\"text-center text-sm text-muted-foreground\">\n\t\t\t\t<Trans>Enter your one-time password.</Trans>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nexport function OtpRequestForm() {\n\tconst [isLoading, setIsLoading] = useState<boolean>(false)\n\tconst [email, setEmail] = useState(\"\")\n\tconst [otpId, setOtpId] = useState<string | undefined>()\n\n\tconst handleSubmit = useCallback(\n\t\tasync (e: React.FormEvent<HTMLFormElement>) => {\n\t\t\te.preventDefault()\n\t\t\tsetIsLoading(true)\n\t\t\ttry {\n\t\t\t\t// console.log(email)\n\t\t\t\tconst { otpId } = await pb.collection(\"users\").requestOTP(email)\n\t\t\t\tsetOtpId(otpId)\n\t\t\t} catch (e: any) {\n\t\t\t\tshowLoginFaliedToast(e?.message)\n\t\t\t} finally {\n\t\t\t\tsetIsLoading(false)\n\t\t\t\tsetEmail(\"\")\n\t\t\t}\n\t\t},\n\t\t[email]\n\t)\n\n\tif (otpId) {\n\t\treturn <OtpInputForm otpId={otpId} mfaId={\"\"} />\n\t}\n\n\treturn (\n\t\t<form onSubmit={handleSubmit}>\n\t\t\t<div className=\"grid gap-3\">\n\t\t\t\t<div className=\"grid gap-1 relative\">\n\t\t\t\t\t<MailIcon className=\"absolute left-3 top-3 h-4 w-4 text-muted-foreground\" />\n\t\t\t\t\t<Label className=\"sr-only\" htmlFor=\"email\">\n\t\t\t\t\t\t<Trans>Email</Trans>\n\t\t\t\t\t</Label>\n\t\t\t\t\t<Input\n\t\t\t\t\t\tvalue={email}\n\t\t\t\t\t\tonChange={(e) => setEmail(e.target.value)}\n\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t\tname=\"email\"\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tplaceholder=\"name@example.com\"\n\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\tautoCapitalize=\"none\"\n\t\t\t\t\t\tautoComplete=\"email\"\n\t\t\t\t\t\tautoCorrect=\"off\"\n\t\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t\t\tclassName=\"ps-9\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t<button className={cn(buttonVariants())} disabled={isLoading}>\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t<LoaderCircle className=\"me-2 h-4 w-4 animate-spin\" />\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<SendHorizonalIcon className=\"me-2 h-4 w-4\" />\n\t\t\t\t\t)}\n\t\t\t\t\t<Trans>Request OTP</Trans>\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t</form>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/logo.tsx",
    "content": "import { useId } from \"react\"\n\nconst d = \"M146.4 73.1h-30.5V59.8h30.5a3.2 3.2 0 0 0 2.3-1 3.2 3.2 0 0 0 1-2.3q0-.8-.3-1.3a1.5 1.5 0 0 0-.7-.6 4.7 4.7 0 0 0-1-.3l-1.3-.1h-13.9q-3.4 0-6.5-1.3-3-1.3-5.2-3.6a16.9 16.9 0 0 1-3.6-5.3 16.3 16.3 0 0 1-1.3-6.5 16.4 16.4 0 0 1 1.3-6.4q1.3-3.1 3.6-5.4 2.2-2.2 5.2-3.5a16.3 16.3 0 0 1 6.5-1.3h27v13.3h-27a3.2 3.2 0 0 0-2.3 1 3.2 3.2 0 0 0-1 2.3 3.3 3.3 0 0 0 1 2.4 3.3 3.3 0 0 0 1.2.8 3.2 3.2 0 0 0 1.1.2h13.9a18.1 18.1 0 0 1 6 1 17.3 17.3 0 0 1 .4.2q3 1.1 5.3 3.2a15.1 15.1 0 0 1 3.6 4.9 14.7 14.7 0 0 1 1.3 5.4 17.2 17.2 0 0 1 0 .9 16 16 0 0 1-1 5.8 15.4 15.4 0 0 1-.3.7 17.3 17.3 0 0 1-3.6 5.2 16.4 16.4 0 0 1-5.3 3.6 16.2 16.2 0 0 1-6.4 1.3Zm64.5-13.3v13.3h-43.6l22-39h-22V21h43.6l-22 39h22ZM35 73.1H0v-70h35q4.4 0 8.2 1.6a21.4 21.4 0 0 1 6.6 4.6q2.9 2.8 4.5 6.6 1.7 3.8 1.7 8.2a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5 1.4 1.6 2.4 3.5a18.3 18.3 0 0 1 1.5 4A17.4 17.4 0 0 1 56 51a15.3 15.3 0 0 1 0 1.1q0 4.3-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.5-3.8 1.7-8.2 1.7Zm76-43L86 60.4l1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.6-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8 26.7 26.7 0 0 1-5.5-8.3 30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8Zm152.3 0-25 30.2 1.5.3a16.7 16.7 0 0 0 1.6 0q2 0 3.8-.4 1.8-.6 3.4-1.6 1.5-1 2.8-2.4a12.8 12.8 0 0 0 2-3.2l9.8 9.8q-1.9 2.6-4.3 4.7a27 27 0 0 1-5.2 3.6 26.1 26.1 0 0 1-6 2.2 26.8 26.8 0 0 1-6.3.8 26.4 26.4 0 0 1-10.4-2 26.2 26.2 0 0 1-8.5-5.8A26.7 26.7 0 0 1 217 58a30.4 30.4 0 0 1-.2-.4q-2.1-5-2.1-11.1a31.9 31.9 0 0 1 .7-7 27 27 0 0 1 1.4-4.3 27 27 0 0 1 3.8-6.6 24.5 24.5 0 0 1 2-2.2 26 26 0 0 1 8.4-5.6 27 27 0 0 1 10.4-2 26.3 26.3 0 0 1 6.4.8 26.9 26.9 0 0 1 6 2.2q2.7 1.5 5.2 3.6 2.4 2.1 4.3 4.8ZM283.4 0v73.1H270V0h13.4ZM14 17v14.1h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 30 40 29a6.9 6.9 0 0 0 1.5-2.3q.5-1.3.5-2.7a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.5q-.6-1.2-1.5-2.2a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.5 7.9 7.9 0 0 0-.2 0H14Zm0 28.1v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.2Q39 58 40 57.1a7 7 0 0 0 1.5-2.3 6.9 6.9 0 0 0 .5-2.5 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 48 40 47a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm63.3 8.3 15.5-20.6a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.3.9 14.7 14.7 0 0 0-1 3.5 18.7 18.7 0 0 0 0 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 0 .1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Zm152.3 0L245 32.8a8 8 0 0 0-1.4-.4 7 7 0 0 0-.4 0 17.2 17.2 0 0 0-1.6-.1 19.2 19.2 0 0 0-.3 0 13.3 13.3 0 0 0-5.1 1q-2.5 1-4.2 2.8a13.1 13.1 0 0 0-2.5 3.6 15.5 15.5 0 0 0-.4.9 14.7 14.7 0 0 0-.8 3.5 18.7 18.7 0 0 0-.2 2.4 17.6 17.6 0 0 0 0 .7v.8a29.4 29.4 0 0 0 .1.1 19.2 19.2 0 0 0 .2 2 20.2 20.2 0 0 0 .4 1.6 18.6 18.6 0 0 0 0 .2 7.5 7.5 0 0 0 .4.9 6 6 0 0 0 .3.6Z\"\n\nexport function Logo({ className }: { className?: string }) {\n\tconst id = useId()\n\n\treturn (\n\t\t// Righteous font from Google Fonts\n\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 285 75\" className={className}>\n\t\t\t<defs>\n\t\t\t\t<linearGradient id={id} x1=\"0%\" y1=\"20%\" x2=\"100%\" y2=\"120%\">\n\t\t\t\t\t<stop offset=\"10%\" style={{ stopColor: \"#747bff\" }} />\n\t\t\t\t\t<stop offset=\"90%\" style={{ stopColor: \"#24eb5c\" }} />\n\t\t\t\t</linearGradient>\n\t\t\t</defs>\n\t\t\t<path\n\t\t\t\tclassName=\"duration-250 group-hover:opacity-0 group-hover:ease-in ease-out\"\n\t\t\t\td={d}\n\t\t\t/>\n\t\t\t<path\n\t\t\t\tclassName=\"opacity-0 duration-250 group-hover:opacity-100 ease-in-out\"\n\t\t\t\tfill={`url(#${id})`}\n\t\t\t\td={d}\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/mode-toggle.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { MoonStarIcon, SunIcon } from \"lucide-react\"\nimport { useTheme } from \"@/components/theme-provider\"\nimport { Button } from \"@/components/ui/button\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./ui/tooltip\"\nimport { Trans } from \"@lingui/react/macro\"\n\nexport function ModeToggle() {\n\tconst { theme, setTheme } = useTheme()\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger>\n\t\t\t\t<Button\n\t\t\t\t\tvariant={\"ghost\"}\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\taria-label={t`Toggle theme`}\n\t\t\t\t\tonClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n\t\t\t\t>\n\t\t\t\t\t<SunIcon className=\"h-[1.2rem] w-[1.2rem] transition-all -rotate-90 dark:opacity-0 dark:rotate-0\" />\n\t\t\t\t\t<MoonStarIcon className=\"absolute h-[1.2rem] w-[1.2rem] transition-all opacity-0 -rotate-90 dark:opacity-100 dark:rotate-0\" />\n\t\t\t\t</Button>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent>\n\t\t\t\t<Trans>Toggle theme</Trans>\n\t\t\t</TooltipContent>\n\t\t</Tooltip>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/navbar.tsx",
    "content": "import { Trans } from \"@lingui/react/macro\"\nimport { getPagePath } from \"@nanostores/router\"\nimport {\n\tContainerIcon,\n\tDatabaseBackupIcon,\n\tHardDriveIcon,\n\tLogOutIcon,\n\tLogsIcon,\n\tSearchIcon,\n\tServerIcon,\n\tSettingsIcon,\n\tUserIcon,\n\tUsersIcon,\n} from \"lucide-react\"\nimport { lazy, Suspense, useState } from \"react\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuGroup,\n\tDropdownMenuItem,\n\tDropdownMenuLabel,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { isAdmin, isReadOnlyUser, logOut, pb } from \"@/lib/api\"\nimport { cn, runOnce } from \"@/lib/utils\"\nimport { AddSystemButton } from \"./add-system\"\nimport { LangToggle } from \"./lang-toggle\"\nimport { Logo } from \"./logo\"\nimport { ModeToggle } from \"./mode-toggle\"\nimport { $router, basePath, Link, prependBasePath } from \"./router\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./ui/tooltip\"\n\nconst CommandPalette = lazy(() => import(\"./command-palette\"))\n\nconst isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0\n\nexport default function Navbar() {\n\treturn (\n\t\t<div className=\"flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4\">\n\t\t\t<Link\n\t\t\t\thref={basePath}\n\t\t\t\taria-label=\"Home\"\n\t\t\t\tclassName=\"p-2 ps-0 me-3 group\"\n\t\t\t\tonMouseEnter={runOnce(() => import(\"@/components/routes/home\"))}\n\t\t\t>\n\t\t\t\t<Logo className=\"h-[1.1rem] md:h-5 fill-foreground\" />\n\t\t\t</Link>\n\t\t\t<SearchButton />\n\n\t\t\t{/** biome-ignore lint/a11y/noStaticElementInteractions: ignore */}\n\t\t\t<div className=\"flex items-center ms-auto\" onMouseEnter={() => import(\"@/components/routes/settings/general\")}>\n\t\t\t\t<Tooltip>\n\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\thref={getPagePath($router, \"containers\")}\n\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"ghost\", size: \"icon\" }))}\n\t\t\t\t\t\t\taria-label=\"Containers\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ContainerIcon className=\"h-[1.2rem] w-[1.2rem]\" strokeWidth={1.5} />\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t<Trans>All Containers</Trans>\n\t\t\t\t\t</TooltipContent>\n\t\t\t\t</Tooltip>\n\t\t\t\t<Tooltip>\n\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\thref={getPagePath($router, \"smart\")}\n\t\t\t\t\t\t\tclassName={cn(\"hidden md:grid\", buttonVariants({ variant: \"ghost\", size: \"icon\" }))}\n\t\t\t\t\t\t\taria-label=\"S.M.A.R.T.\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<HardDriveIcon className=\"h-[1.2rem] w-[1.2rem]\" strokeWidth={1.5} />\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t<TooltipContent>S.M.A.R.T.</TooltipContent>\n\t\t\t\t</Tooltip>\n\t\t\t\t<LangToggle />\n\t\t\t\t<ModeToggle />\n\t\t\t\t<Tooltip>\n\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\thref={getPagePath($router, \"settings\", { name: \"general\" })}\n\t\t\t\t\t\t\taria-label=\"Settings\"\n\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"ghost\", size: \"icon\" }))}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<SettingsIcon className=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t<Trans>Settings</Trans>\n\t\t\t\t\t</TooltipContent>\n\t\t\t\t</Tooltip>\n\t\t\t\t<DropdownMenu>\n\t\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t\t<button aria-label=\"User Actions\" className={cn(buttonVariants({ variant: \"ghost\", size: \"icon\" }))}>\n\t\t\t\t\t\t\t<UserIcon className=\"h-[1.2rem] w-[1.2rem]\" />\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t\t<DropdownMenuContent align={isReadOnlyUser() ? \"end\" : \"center\"} className=\"min-w-44\">\n\t\t\t\t\t\t<DropdownMenuLabel>{pb.authStore.record?.email}</DropdownMenuLabel>\n\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t<DropdownMenuGroup>\n\t\t\t\t\t\t\t{isAdmin() && (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<DropdownMenuItem asChild>\n\t\t\t\t\t\t\t\t\t\t<a href={prependBasePath(\"/_/\")} target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<UsersIcon className=\"me-2.5 h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Users</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<DropdownMenuItem asChild>\n\t\t\t\t\t\t\t\t\t\t<a href={prependBasePath(\"/_/#/collections?collection=systems\")} target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<ServerIcon className=\"me-2.5 h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Systems</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<DropdownMenuItem asChild>\n\t\t\t\t\t\t\t\t\t\t<a href={prependBasePath(\"/_/#/logs\")} target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<LogsIcon className=\"me-2.5 h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Logs</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<DropdownMenuItem asChild>\n\t\t\t\t\t\t\t\t\t\t<a href={prependBasePath(\"/_/#/settings/backups\")} target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\t\t<DatabaseBackupIcon className=\"me-2.5 h-4 w-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Backups</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</DropdownMenuGroup>\n\t\t\t\t\t\t<DropdownMenuItem onSelect={logOut}>\n\t\t\t\t\t\t\t<LogOutIcon className=\"me-2.5 h-4 w-4\" />\n\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t<Trans>Log Out</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t</DropdownMenuContent>\n\t\t\t\t</DropdownMenu>\n\t\t\t\t<AddSystemButton className=\"ms-2\" />\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nconst Kbd = ({ children }: { children: React.ReactNode }) => (\n\t<kbd className=\"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100\">\n\t\t{children}\n\t</kbd>\n)\n\nfunction SearchButton() {\n\tconst [open, setOpen] = useState(false)\n\n\treturn (\n\t\t<>\n\t\t\t<Button\n\t\t\t\tvariant=\"outline\"\n\t\t\t\tclassName=\"hidden md:block text-sm text-muted-foreground px-4\"\n\t\t\t\tonClick={() => setOpen(true)}\n\t\t\t>\n\t\t\t\t<span className=\"flex items-center\">\n\t\t\t\t\t<SearchIcon className=\"me-1.5 h-4 w-4\" />\n\t\t\t\t\t<Trans>Search</Trans>\n\t\t\t\t\t<span className=\"flex items-center ms-3.5\">\n\t\t\t\t\t\t<Kbd>{isMac ? \"⌘\" : \"Ctrl\"}</Kbd>\n\t\t\t\t\t\t<Kbd>K</Kbd>\n\t\t\t\t\t</span>\n\t\t\t\t</span>\n\t\t\t</Button>\n\t\t\t<Suspense>\n\t\t\t\t<CommandPalette open={open} setOpen={setOpen} />\n\t\t\t</Suspense>\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/router.tsx",
    "content": "import { createRouter } from \"@nanostores/router\"\n\nconst routes = {\n\thome: \"/\",\n\tcontainers: \"/containers\",\n\tsmart: \"/smart\",\n\tsystem: `/system/:id`,\n\tsettings: `/settings/:name?`,\n\tforgot_password: `/forgot-password`,\n\trequest_otp: `/request-otp`,\n} as const\n\n/**\n * The base path of the application.\n * This is used to prepend the base path to all routes.\n */\nexport const basePath = BESZEL?.BASE_PATH || \"\"\n\n/**\n * Prepends the base path to the given path.\n * @param path The path to prepend the base path to.\n * @returns The path with the base path prepended.\n */\nexport const prependBasePath = (path: string) => (basePath + path).replaceAll(\"//\", \"/\")\n\n// prepend base path to routes\nfor (const route in routes) {\n\t// @ts-expect-error need as const above to get nanostores to parse types properly\n\troutes[route] = prependBasePath(routes[route])\n}\n\nexport const $router = createRouter(routes, { links: false })\n\n/** Navigate to url using router\n *  Base path is automatically prepended if serving from subpath\n */\nexport const navigate = (urlString: string) => {\n\t$router.open(urlString)\n}\n\nexport function Link(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {\n\treturn (\n\t\t<a\n\t\t\t{...props}\n\t\t\tonClick={(e) => {\n\t\t\t\te.preventDefault()\n\t\t\t\tconst href = props.href || \"\"\n\t\t\t\tif (e.ctrlKey || e.metaKey) {\n\t\t\t\t\twindow.open(href, \"_blank\")\n\t\t\t\t} else {\n\t\t\t\t\tnavigate(href)\n\t\t\t\t\tprops.onClick?.(e)\n\t\t\t\t}\n\t\t\t}}\n\t\t></a>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/containers.tsx",
    "content": "import { useLingui } from \"@lingui/react/macro\"\nimport { memo, useEffect, useMemo } from \"react\"\nimport ContainersTable from \"@/components/containers-table/containers-table\"\nimport { ActiveAlerts } from \"@/components/active-alerts\"\nimport { FooterRepoLink } from \"@/components/footer-repo-link\"\n\nexport default memo(() => {\n\tconst { t } = useLingui()\n\n\tuseEffect(() => {\n\t\tdocument.title = `${t`All Containers`} / Beszel`\n\t}, [t])\n\n\treturn useMemo(\n\t\t() => (\n\t\t\t<>\n\t\t\t\t<div className=\"grid gap-4\">\n\t\t\t\t\t<ActiveAlerts />\n\t\t\t\t\t<ContainersTable />\n\t\t\t\t</div>\n\t\t\t\t<FooterRepoLink />\n\t\t\t</>\n\t\t),\n\t\t[]\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/routes/home.tsx",
    "content": "import { useLingui } from \"@lingui/react/macro\"\nimport { memo, Suspense, useEffect, useMemo } from \"react\"\nimport SystemsTable from \"@/components/systems-table/systems-table\"\nimport { ActiveAlerts } from \"@/components/active-alerts\"\nimport { FooterRepoLink } from \"@/components/footer-repo-link\"\n\nexport default memo(() => {\n\tconst { t } = useLingui()\n\n\tuseEffect(() => {\n\t\tdocument.title = `${t`All Systems`} / Beszel`\n\t}, [t])\n\n\treturn useMemo(\n\t\t() => (\n\t\t\t<>\n\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t<ActiveAlerts />\n\t\t\t\t\t<Suspense>\n\t\t\t\t\t\t<SystemsTable />\n\t\t\t\t\t</Suspense>\n\t\t\t\t</div>\n\t\t\t\t<FooterRepoLink />\n\t\t\t</>\n\t\t),\n\t\t[]\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/alerts-history-data-table.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport {\n\ttype ColumnFiltersState,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetFilteredRowModel,\n\tgetPaginationRowModel,\n\tgetSortedRowModel,\n\ttype PaginationState,\n\ttype SortingState,\n\tuseReactTable,\n\ttype VisibilityState,\n} from \"@tanstack/react-table\"\nimport {\n\tChevronLeftIcon,\n\tChevronRightIcon,\n\tChevronsLeftIcon,\n\tChevronsRightIcon,\n\tDownloadIcon,\n\tTrash2Icon,\n} from \"lucide-react\"\nimport { memo, useEffect, useState } from \"react\"\nimport {\n\tAlertDialog,\n\tAlertDialogAction,\n\tAlertDialogCancel,\n\tAlertDialogContent,\n\tAlertDialogDescription,\n\tAlertDialogFooter,\n\tAlertDialogHeader,\n\tAlertDialogTitle,\n\tAlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { useToast } from \"@/components/ui/use-toast\"\nimport { alertInfo } from \"@/lib/alerts\"\nimport { pb } from \"@/lib/api\"\nimport { cn, formatDuration, formatShortDate, useBrowserStorage } from \"@/lib/utils\"\nimport type { AlertsHistoryRecord } from \"@/types\"\nimport { alertsHistoryColumns } from \"../../alerts-history-columns\"\n\nconst SectionIntro = memo(() => {\n\treturn (\n\t\t<div>\n\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t<Trans>Alert History</Trans>\n\t\t\t</h3>\n\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t<Trans>View your 200 most recent alerts.</Trans>\n\t\t\t</p>\n\t\t</div>\n\t)\n})\n\nexport default function AlertsHistoryDataTable() {\n\tconst [data, setData] = useState<AlertsHistoryRecord[]>([])\n\tconst [sorting, setSorting] = useState<SortingState>([])\n\tconst [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n\tconst [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})\n\tconst [rowSelection, setRowSelection] = useState({})\n\tconst [globalFilter, setGlobalFilter] = useState(\"\")\n\tconst { toast } = useToast()\n\tconst [deleteOpen, setDeleteDialogOpen] = useState(false)\n\t\n\t// Store pagination preference in local storage\n\tconst [pagination, setPagination] = useBrowserStorage<PaginationState>(\"ah-pagination\", {\n\t\tpageIndex: 0,\n\t\tpageSize: 10,\n\t})\n\n\tuseEffect(() => {\n\t\tlet unsubscribe: (() => void) | undefined\n\t\tconst pbOptions = {\n\t\t\texpand: \"system\",\n\t\t\tfields: \"id,name,value,state,created,resolved,expand.system.name\",\n\t\t}\n\t\t// Initial load\n\t\tpb.collection<AlertsHistoryRecord>(\"alerts_history\")\n\t\t\t.getList(0, 200, {\n\t\t\t\t...pbOptions,\n\t\t\t\tsort: \"-created\",\n\t\t\t})\n\t\t\t.then(({ items }) => setData(items))\n\n\t\t// Subscribe to changes\n\t\t;(async () => {\n\t\t\tunsubscribe = await pb.collection(\"alerts_history\").subscribe(\n\t\t\t\t\"*\",\n\t\t\t\t(e) => {\n\t\t\t\t\tif (e.action === \"create\") {\n\t\t\t\t\t\tsetData((current) => [e.record as AlertsHistoryRecord, ...current])\n\t\t\t\t\t}\n\t\t\t\t\tif (e.action === \"update\") {\n\t\t\t\t\t\tsetData((current) => current.map((r) => (r.id === e.record.id ? (e.record as AlertsHistoryRecord) : r)))\n\t\t\t\t\t}\n\t\t\t\t\tif (e.action === \"delete\") {\n\t\t\t\t\t\tsetData((current) => current.filter((r) => r.id !== e.record.id))\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tpbOptions\n\t\t\t)\n\t\t})()\n\t\t// Unsubscribe on unmount\n\t\treturn () => unsubscribe?.()\n\t}, [])\n\n\tconst table = useReactTable({\n\t\tdata,\n\t\tcolumns: [\n\t\t\t{\n\t\t\t\tid: \"select\",\n\t\t\t\theader: ({ table }) => (\n\t\t\t\t\t<Checkbox\n\t\t\t\t\t\tclassName=\"ms-2\"\n\t\t\t\t\t\tchecked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && \"indeterminate\")}\n\t\t\t\t\t\tonCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n\t\t\t\t\t\taria-label=\"Select all\"\n\t\t\t\t\t/>\n\t\t\t\t),\n\t\t\t\tcell: ({ row }) => (\n\t\t\t\t\t<Checkbox\n\t\t\t\t\t\tchecked={row.getIsSelected()}\n\t\t\t\t\t\tonCheckedChange={(value) => row.toggleSelected(!!value)}\n\t\t\t\t\t\taria-label=\"Select row\"\n\t\t\t\t\t/>\n\t\t\t\t),\n\t\t\t\tenableSorting: false,\n\t\t\t\tenableHiding: false,\n\t\t\t},\n\t\t\t...alertsHistoryColumns,\n\t\t],\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tgetPaginationRowModel: getPaginationRowModel(),\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tonSortingChange: setSorting,\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tonColumnVisibilityChange: setColumnVisibility,\n\t\tonRowSelectionChange: setRowSelection,\n\t\tonPaginationChange: setPagination,\n\t\tstate: {\n\t\t\tsorting,\n\t\t\tcolumnFilters,\n\t\t\tcolumnVisibility,\n\t\t\trowSelection,\n\t\t\tglobalFilter,\n\t\t\tpagination,\n\t\t},\n\t\tonGlobalFilterChange: setGlobalFilter,\n\t\tglobalFilterFn: (row, _columnId, filterValue) => {\n\t\t\tconst system = row.original.expand?.system?.name ?? \"\"\n\t\t\tconst name = row.getValue(\"name\") ?? \"\"\n\t\t\tconst created = row.getValue(\"created\") ?? \"\"\n\t\t\tconst search = String(filterValue).toLowerCase()\n\t\t\treturn (\n\t\t\t\tsystem.toLowerCase().includes(search) ||\n\t\t\t\t(name as string).toLowerCase().includes(search) ||\n\t\t\t\t(created as string).toLowerCase().includes(search)\n\t\t\t)\n\t\t},\n\t})\n\n\t// Bulk delete handler\n\tconst handleBulkDelete = async () => {\n\t\tsetDeleteDialogOpen(false)\n\t\tconst selectedIds = table.getSelectedRowModel().rows.map((row) => row.original.id)\n\t\ttry {\n\t\t\tlet batch = pb.createBatch()\n\t\t\tlet inBatch = 0\n\t\t\tfor (const id of selectedIds) {\n\t\t\t\tbatch.collection(\"alerts_history\").delete(id)\n\t\t\t\tinBatch++\n\t\t\t\tif (inBatch > 20) {\n\t\t\t\t\tawait batch.send()\n\t\t\t\t\tbatch = pb.createBatch()\n\t\t\t\t\tinBatch = 0\n\t\t\t\t}\n\t\t\t}\n\t\t\tinBatch && (await batch.send())\n\t\t\ttable.resetRowSelection()\n\t\t} catch (e) {\n\t\t\ttoast({\n\t\t\t\tvariant: \"destructive\",\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: `Failed to delete records.`,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Export to CSV handler\n\tconst handleExportCSV = () => {\n\t\tconst selectedRows = table.getSelectedRowModel().rows\n\t\tif (!selectedRows.length) return\n\t\tconst cells: Record<string, (record: AlertsHistoryRecord) => string> = {\n\t\t\tsystem: (record) => record.expand?.system?.name || record.system,\n\t\t\tname: (record) => alertInfo[record.name]?.name() || record.name,\n\t\t\tvalue: (record) => record.value + (alertInfo[record.name]?.unit ?? \"\"),\n\t\t\tstate: (record) => (record.resolved ? t`Resolved` : t`Active`),\n\t\t\tcreated: (record) => formatShortDate(record.created),\n\t\t\tresolved: (record) => (record.resolved ? formatShortDate(record.resolved) : \"\"),\n\t\t\tduration: (record) => (record.resolved ? formatDuration(record.created, record.resolved) : \"\"),\n\t\t}\n\t\tconst csvRows = [Object.keys(cells).join(\",\")]\n\t\tfor (const row of selectedRows) {\n\t\t\tconst r = row.original\n\t\t\tcsvRows.push(\n\t\t\t\tObject.values(cells)\n\t\t\t\t\t.map((val) => val(r))\n\t\t\t\t\t.join(\",\")\n\t\t\t)\n\t\t}\n\t\tconst blob = new Blob([csvRows.join(\"\\n\")], { type: \"text/csv\" })\n\t\tconst url = URL.createObjectURL(blob)\n\t\tconst a = document.createElement(\"a\")\n\t\ta.href = url\n\t\ta.download = \"alerts_history.csv\"\n\t\ta.click()\n\t\tURL.revokeObjectURL(url)\n\t}\n\n\treturn (\n\t\t<div className=\"@container w-full\">\n\t\t\t<div className=\"@3xl:flex items-end mb-4 gap-4\">\n\t\t\t\t<SectionIntro />\n\t\t\t\t<div className=\"flex items-center gap-2 ms-auto mt-3 @3xl:mt-0\">\n\t\t\t\t\t{table.getFilteredSelectedRowModel().rows.length > 0 && (\n\t\t\t\t\t\t<div className=\"fixed bottom-0 left-0 w-full p-4 grid grid-cols-2 items-center gap-4 z-50 backdrop-blur-md shrink-0 @lg:static @lg:p-0 @lg:w-auto @lg:gap-3\">\n\t\t\t\t\t\t\t<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteDialogOpen(open)}>\n\t\t\t\t\t\t\t\t<AlertDialogTrigger asChild>\n\t\t\t\t\t\t\t\t\t<Button variant=\"destructive\" className=\"h-9 shrink-0\">\n\t\t\t\t\t\t\t\t\t\t<Trash2Icon className=\"size-4 shrink-0\" />\n\t\t\t\t\t\t\t\t\t\t<span className=\"ms-1\">\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Delete</Trans>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t</AlertDialogTrigger>\n\t\t\t\t\t\t\t\t<AlertDialogContent>\n\t\t\t\t\t\t\t\t\t<AlertDialogHeader>\n\t\t\t\t\t\t\t\t\t\t<AlertDialogTitle>\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Are you sure?</Trans>\n\t\t\t\t\t\t\t\t\t\t</AlertDialogTitle>\n\t\t\t\t\t\t\t\t\t\t<AlertDialogDescription>\n\t\t\t\t\t\t\t\t\t\t\t<Trans>This will permanently delete all selected records from the database.</Trans>\n\t\t\t\t\t\t\t\t\t\t</AlertDialogDescription>\n\t\t\t\t\t\t\t\t\t</AlertDialogHeader>\n\t\t\t\t\t\t\t\t\t<AlertDialogFooter>\n\t\t\t\t\t\t\t\t\t\t<AlertDialogCancel>\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Cancel</Trans>\n\t\t\t\t\t\t\t\t\t\t</AlertDialogCancel>\n\t\t\t\t\t\t\t\t\t\t<AlertDialogAction\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"destructive\" }))}\n\t\t\t\t\t\t\t\t\t\t\tonClick={handleBulkDelete}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Continue</Trans>\n\t\t\t\t\t\t\t\t\t\t</AlertDialogAction>\n\t\t\t\t\t\t\t\t\t</AlertDialogFooter>\n\t\t\t\t\t\t\t\t</AlertDialogContent>\n\t\t\t\t\t\t\t</AlertDialog>\n\t\t\t\t\t\t\t<Button variant=\"outline\" className=\"h-10\" onClick={handleExportCSV}>\n\t\t\t\t\t\t\t\t<DownloadIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t<span className=\"ms-1\">\n\t\t\t\t\t\t\t\t\t<Trans>Export</Trans>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t<Input\n\t\t\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\t\t\tvalue={globalFilter}\n\t\t\t\t\t\tonChange={(e) => setGlobalFilter(e.target.value)}\n\t\t\t\t\t\tclassName=\"px-4 w-full max-w-full @3xl:w-64\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div className=\"rounded-md border overflow-x-auto whitespace-nowrap\">\n\t\t\t\t<Table>\n\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t\t\t\t<tr key={headerGroup.id} className=\"border-border/50\">\n\t\t\t\t\t\t\t\t{headerGroup.headers.map((header) => (\n\t\t\t\t\t\t\t\t\t<TableHead className=\"px-2\" key={header.id}>\n\t\t\t\t\t\t\t\t\t\t{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</TableHeader>\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{table.getRowModel().rows.length ? (\n\t\t\t\t\t\t\ttable.getRowModel().rows.map((row) => (\n\t\t\t\t\t\t\t\t<TableRow key={row.id} data-state={row.getIsSelected() && \"selected\"}>\n\t\t\t\t\t\t\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t\t\t\t\t\t\t<TableCell key={cell.id} className=\"py-3\">\n\t\t\t\t\t\t\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t<TableCell colSpan={table.getAllColumns().length} className=\"h-24 text-center\">\n\t\t\t\t\t\t\t\t\t<Trans>No results.</Trans>\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</Table>\n\t\t\t</div>\n\t\t\t<div className=\"flex items-center justify-between ps-1 tabular-nums\">\n\t\t\t\t<div className=\"text-muted-foreground hidden flex-1 text-sm lg:flex\">\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\t{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)\n\t\t\t\t\t\tselected.\n\t\t\t\t\t</Trans>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex w-full items-center gap-8 lg:w-fit my-3\">\n\t\t\t\t\t<div className=\"hidden items-center gap-2 lg:flex\">\n\t\t\t\t\t\t<Label htmlFor=\"rows-per-page\" className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t<Trans>Rows per page</Trans>\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\tvalue={`${table.getState().pagination.pageSize}`}\n\t\t\t\t\t\t\tonValueChange={(value) => {\n\t\t\t\t\t\t\t\ttable.setPageSize(Number(value));\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<SelectTrigger className=\"w-18\" id=\"rows-per-page\">\n\t\t\t\t\t\t\t\t<SelectValue placeholder={table.getState().pagination.pageSize} />\n\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t<SelectContent side=\"top\">\n\t\t\t\t\t\t\t\t{[10, 20, 50, 100, 200].map((pageSize) => (\n\t\t\t\t\t\t\t\t\t<SelectItem key={pageSize} value={`${pageSize}`}>\n\t\t\t\t\t\t\t\t\t\t{pageSize}\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t</Select>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex w-fit items-center justify-center text-sm font-medium\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tPage {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"ms-auto flex items-center gap-2 lg:ms-0\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"hidden size-9 p-0 lg:flex\"\n\t\t\t\t\t\t\tonClick={() => table.setPageIndex(0)}\n\t\t\t\t\t\t\tdisabled={!table.getCanPreviousPage()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"sr-only\">Go to first page</span>\n\t\t\t\t\t\t\t<ChevronsLeftIcon className=\"size-5\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"size-9\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonClick={() => table.previousPage()}\n\t\t\t\t\t\t\tdisabled={!table.getCanPreviousPage()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"sr-only\">Go to previous page</span>\n\t\t\t\t\t\t\t<ChevronLeftIcon className=\"size-5\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"size-9\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonClick={() => table.nextPage()}\n\t\t\t\t\t\t\tdisabled={!table.getCanNextPage()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"sr-only\">Go to next page</span>\n\t\t\t\t\t\t\t<ChevronRightIcon className=\"size-5\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"hidden size-9 lg:flex\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tonClick={() => table.setPageIndex(table.getPageCount() - 1)}\n\t\t\t\t\t\t\tdisabled={!table.getCanNextPage()}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"sr-only\">Go to last page</span>\n\t\t\t\t\t\t\t<ChevronsRightIcon className=\"size-5\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/config-yaml.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { redirectPage } from \"@nanostores/router\"\nimport clsx from \"clsx\"\nimport { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { $router } from \"@/components/router\"\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Button } from \"@/components/ui/button\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport { isAdmin, pb } from \"@/lib/api\"\n\nexport default function ConfigYaml() {\n\tconst [configContent, setConfigContent] = useState<string>(\"\")\n\tconst [isLoading, setIsLoading] = useState(false)\n\n\tconst ButtonIcon = isLoading ? LoaderCircleIcon : FileSlidersIcon\n\n\tasync function fetchConfig() {\n\t\ttry {\n\t\t\tsetIsLoading(true)\n\t\t\tconst { config } = await pb.send<{ config: string }>(\"/api/beszel/config-yaml\", {})\n\t\t\tsetConfigContent(config)\n\t\t} catch (error: any) {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: error.message,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t} finally {\n\t\t\tsetIsLoading(false)\n\t\t}\n\t}\n\n\tif (!isAdmin()) {\n\t\tredirectPage($router, \"settings\", { name: \"general\" })\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<div>\n\t\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t\t<Trans>YAML Configuration</Trans>\n\t\t\t\t</h3>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Trans>Export your current systems configuration.</Trans>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t<Separator className=\"my-4\" />\n\t\t\t<div className=\"space-y-2\">\n\t\t\t\t<div className=\"mb-4\">\n\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed my-1\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tSystems may be managed in a <code className=\"bg-muted rounded-sm px-1 text-primary\">config.yml</code> file\n\t\t\t\t\t\t\tinside your data directory.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tOn each restart, systems in the database will be updated to match the systems defined in the file.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t\t<Alert className=\"my-4 border-destructive text-destructive w-auto table md:pe-6\">\n\t\t\t\t\t\t<AlertCircleIcon className=\"size-4.5 stroke-destructive\" />\n\t\t\t\t\t\t<AlertTitle>\n\t\t\t\t\t\t\t<Trans>Caution - potential data loss</Trans>\n\t\t\t\t\t\t</AlertTitle>\n\t\t\t\t\t\t<AlertDescription>\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tExisting systems not defined in <code>config.yml</code> will be deleted. Please make regular backups.\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</AlertDescription>\n\t\t\t\t\t</Alert>\n\t\t\t\t</div>\n\t\t\t\t{configContent && (\n\t\t\t\t\t<Textarea\n\t\t\t\t\t\tdir=\"ltr\"\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tdefaultValue={configContent}\n\t\t\t\t\t\tspellCheck=\"false\"\n\t\t\t\t\t\trows={Math.min(25, configContent.split(\"\\n\").length)}\n\t\t\t\t\t\tclassName=\"font-mono whitespace-pre\"\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<Separator className=\"my-5\" />\n\t\t\t<Button type=\"button\" className=\"mt-2 flex items-center gap-1\" onClick={fetchConfig} disabled={isLoading}>\n\t\t\t\t<ButtonIcon className={clsx(\"h-4 w-4 me-0.5\", isLoading && \"animate-spin\")} />\n\t\t\t\t<Trans>Export configuration</Trans>\n\t\t\t</Button>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/general.tsx",
    "content": "/** biome-ignore-all lint/correctness/useUniqueElementIds: component is only rendered once */\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { LanguagesIcon, LoaderCircleIcon, SaveIcon } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { useStore } from \"@nanostores/react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { Separator } from \"@/components/ui/separator\"\nimport Slider from \"@/components/ui/slider\"\nimport { HourFormat, Unit } from \"@/lib/enums\"\nimport { dynamicActivate } from \"@/lib/i18n\"\nimport languages from \"@/lib/languages\"\nimport { $userSettings, defaultLayoutWidth } from \"@/lib/stores\"\nimport { chartTimeData, currentHour12 } from \"@/lib/utils\"\nimport type { UserSettings } from \"@/types\"\nimport { saveSettings } from \"./layout\"\n\nexport default function SettingsProfilePage({ userSettings }: { userSettings: UserSettings }) {\n\tconst [isLoading, setIsLoading] = useState(false)\n\tconst { i18n } = useLingui()\n\tconst currentUserSettings = useStore($userSettings)\n\tconst layoutWidth = currentUserSettings.layoutWidth ?? defaultLayoutWidth\n\n\tasync function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n\t\te.preventDefault()\n\t\tsetIsLoading(true)\n\t\tconst formData = new FormData(e.target as HTMLFormElement)\n\t\tconst data = Object.fromEntries(formData) as Partial<UserSettings>\n\t\tawait saveSettings(data)\n\t\tsetIsLoading(false)\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<div>\n\t\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t\t<Trans>General</Trans>\n\t\t\t\t</h3>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Trans>Change general application options.</Trans>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t<Separator className=\"my-4\" />\n\t\t\t<form onSubmit={handleSubmit} className=\"space-y-5\">\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium flex items-center gap-2\">\n\t\t\t\t\t\t\t<LanguagesIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t<Trans>Language</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\tWant to help improve our translations? Check{\" \"}\n\t\t\t\t\t\t\t\t<a href=\"https://crowdin.com/project/beszel\" className=\"link\" target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t\t\t\tCrowdin\n\t\t\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\t\t\tfor details.\n\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Label className=\"block\" htmlFor=\"lang\">\n\t\t\t\t\t\t<Trans>Preferred Language</Trans>\n\t\t\t\t\t</Label>\n\t\t\t\t\t<Select value={i18n.locale} onValueChange={(lang: string) => dynamicActivate(lang)}>\n\t\t\t\t\t\t<SelectTrigger id=\"lang\">\n\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t{languages.map(([lang, label, e]) => (\n\t\t\t\t\t\t\t\t<SelectItem key={lang} value={lang}>\n\t\t\t\t\t\t\t\t\t<span className=\"me-2.5\">\n\t\t\t\t\t\t\t\t\t\t{e || (\n\t\t\t\t\t\t\t\t\t\t\t<code\n\t\t\t\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"font-mono bg-muted text-[.65em] w-5 h-4 inline-grid place-items-center\"\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{lang}\n\t\t\t\t\t\t\t\t\t\t\t</code>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{label}\n\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t</Select>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t<Trans>Layout width</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<Label htmlFor=\"layoutWidth\" className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t<Trans>Adjust the width of the main layout</Trans> ({layoutWidth}px)\n\t\t\t\t\t\t</Label>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Slider\n\t\t\t\t\t\tid=\"layoutWidth\"\n\t\t\t\t\t\tname=\"layoutWidth\"\n\t\t\t\t\t\tvalue={[layoutWidth]}\n\t\t\t\t\t\tonValueChange={(val) => $userSettings.setKey(\"layoutWidth\", val[0])}\n\t\t\t\t\t\tmin={1000}\n\t\t\t\t\t\tmax={2000}\n\t\t\t\t\t\tstep={10}\n\t\t\t\t\t\tclassName=\"w-full mb-1\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t<Trans>Chart options</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t<Trans>Adjust display options for charts.</Trans>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"grid sm:grid-cols-3 gap-4\">\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label className=\"block\" htmlFor=\"chartTime\">\n\t\t\t\t\t\t\t\t<Trans>Default time period</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select name=\"chartTime\" key={userSettings.chartTime} defaultValue={userSettings.chartTime}>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"chartTime\">\n\t\t\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t{Object.entries(chartTimeData).map(([value, { label }]) => (\n\t\t\t\t\t\t\t\t\t\t<SelectItem key={value} value={value}>\n\t\t\t\t\t\t\t\t\t\t\t{label()}\n\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label className=\"block\" htmlFor=\"hourFormat\">\n\t\t\t\t\t\t\t\t<Trans>Time format</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\tname=\"hourFormat\"\n\t\t\t\t\t\t\t\tkey={userSettings.hourFormat}\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.hourFormat ?? (currentHour12() ? HourFormat[\"12h\"] : HourFormat[\"24h\"])}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"hourFormat\">\n\t\t\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t{Object.keys(HourFormat).map((value) => (\n\t\t\t\t\t\t\t\t\t\t<SelectItem key={value} value={value}>\n\t\t\t\t\t\t\t\t\t\t\t{value}\n\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t<Trans comment=\"Temperature / network units\">Unit preferences</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t<Trans>Change display units for metrics.</Trans>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"grid sm:grid-cols-3 gap-4\">\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label className=\"block\" htmlFor=\"unitTemp\">\n\t\t\t\t\t\t\t\t<Trans>Temperature unit</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\tname=\"unitTemp\"\n\t\t\t\t\t\t\t\tkey={userSettings.unitTemp}\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.unitTemp?.toString() || String(Unit.Celsius)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"unitTemp\">\n\t\t\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Celsius)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Celsius (°C)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Fahrenheit)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Fahrenheit (°F)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label className=\"block\" htmlFor=\"unitNet\">\n\t\t\t\t\t\t\t\t<Trans comment=\"Context: Bytes or bits\">Network unit</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\tname=\"unitNet\"\n\t\t\t\t\t\t\t\tkey={userSettings.unitNet}\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.unitNet?.toString() ?? String(Unit.Bytes)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"unitNet\">\n\t\t\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Bytes)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Bits)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Bits (Kbps, Mbps, Gbps)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label className=\"block\" htmlFor=\"unitDisk\">\n\t\t\t\t\t\t\t\t<Trans>Disk unit</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\t\tname=\"unitDisk\"\n\t\t\t\t\t\t\t\tkey={userSettings.unitDisk}\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.unitDisk?.toString() ?? String(Unit.Bytes)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"unitDisk\">\n\t\t\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Bytes)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Bytes (KB/s, MB/s, GB/s)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t<SelectItem value={String(Unit.Bits)}>\n\t\t\t\t\t\t\t\t\t\t<Trans>Bits (Kbps, Mbps, Gbps)</Trans>\n\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t<Trans>Warning thresholds</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t<Trans>Set percentage thresholds for meter colors.</Trans>\n\t\t\t\t\t\t</p>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"grid grid-cols-2 lg:grid-cols-3 gap-4 items-end\">\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label htmlFor=\"colorWarn\">\n\t\t\t\t\t\t\t\t<Trans>Warning (%)</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tid=\"colorWarn\"\n\t\t\t\t\t\t\t\tname=\"colorWarn\"\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\tmax={100}\n\t\t\t\t\t\t\t\tclassName=\"min-w-24\"\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.colorWarn ?? 65}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"grid gap-1\">\n\t\t\t\t\t\t\t<Label htmlFor=\"colorCrit\">\n\t\t\t\t\t\t\t\t<Trans>Critical (%)</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tid=\"colorCrit\"\n\t\t\t\t\t\t\t\tname=\"colorCrit\"\n\t\t\t\t\t\t\t\ttype=\"number\"\n\t\t\t\t\t\t\t\tmin={1}\n\t\t\t\t\t\t\t\tmax={100}\n\t\t\t\t\t\t\t\tclassName=\"min-w-24\"\n\t\t\t\t\t\t\t\tdefaultValue={userSettings.colorCrit ?? 90}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<Button type=\"submit\" className=\"flex items-center gap-1.5 disabled:opacity-100\" disabled={isLoading}>\n\t\t\t\t\t{isLoading ? <LoaderCircleIcon className=\"h-4 w-4 animate-spin\" /> : <SaveIcon className=\"h-4 w-4\" />}\n\t\t\t\t\t<Trans>Save Settings</Trans>\n\t\t\t\t</Button>\n\t\t\t</form>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/heartbeat.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { redirectPage } from \"@nanostores/router\"\nimport { LoaderCircleIcon, SendIcon } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\nimport { $router } from \"@/components/router\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport { isAdmin, pb } from \"@/lib/api\"\nimport { cn } from \"@/lib/utils\"\n\ninterface HeartbeatStatus {\n\tenabled: boolean\n\turl?: string\n\tinterval?: number\n\tmethod?: string\n\tmsg?: string\n}\n\nexport default function HeartbeatSettings() {\n\tconst [status, setStatus] = useState<HeartbeatStatus | null>(null)\n\tconst [isLoading, setIsLoading] = useState(true)\n\tconst [isTesting, setIsTesting] = useState(false)\n\n\tif (!isAdmin()) {\n\t\tredirectPage($router, \"settings\", { name: \"general\" })\n\t}\n\n\tuseEffect(() => {\n\t\tfetchStatus()\n\t}, [])\n\n\tasync function fetchStatus() {\n\t\ttry {\n\t\t\tsetIsLoading(true)\n\t\t\tconst res = await pb.send<HeartbeatStatus>(\"/api/beszel/heartbeat-status\", {})\n\t\t\tsetStatus(res)\n\t\t} catch (error: unknown) {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: (error as Error).message,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t} finally {\n\t\t\tsetIsLoading(false)\n\t\t}\n\t}\n\n\tasync function sendTestHeartbeat() {\n\t\tsetIsTesting(true)\n\t\ttry {\n\t\t\tconst res = await pb.send<{ err: string | false }>(\"/api/beszel/test-heartbeat\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t})\n\t\t\tif (\"err\" in res && !res.err) {\n\t\t\t\ttoast({\n\t\t\t\t\ttitle: t`Heartbeat sent successfully`,\n\t\t\t\t\tdescription: t`Check your monitoring service`,\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\ttoast({\n\t\t\t\t\ttitle: t`Error`,\n\t\t\t\t\tdescription: (res.err as string) ?? t`Failed to send heartbeat`,\n\t\t\t\t\tvariant: \"destructive\",\n\t\t\t\t})\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: (error as Error).message,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t} finally {\n\t\t\tsetIsTesting(false)\n\t\t}\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<div>\n\t\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t\t<Trans>Heartbeat Monitoring</Trans>\n\t\t\t\t</h3>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\tSend periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it\n\t\t\t\t\t\tto the internet.\n\t\t\t\t\t</Trans>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t<Separator className=\"my-4\" />\n\n\t\t\t{status?.enabled ? (\n\t\t\t\t<EnabledState status={status} isTesting={isTesting} sendTestHeartbeat={sendTestHeartbeat} />\n\t\t\t) : (\n\t\t\t\t<NotEnabledState isLoading={isLoading} />\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction EnabledState({\n\tstatus,\n\tisTesting,\n\tsendTestHeartbeat,\n}: {\n\tstatus: HeartbeatStatus\n\tisTesting: boolean\n\tsendTestHeartbeat: () => void\n}) {\n\tconst TestIcon = isTesting ? LoaderCircleIcon : SendIcon\n\treturn (\n\t\t<div className=\"space-y-5\">\n\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t<Badge variant=\"success\">\n\t\t\t\t\t<Trans>Active</Trans>\n\t\t\t\t</Badge>\n\t\t\t</div>\n\t\t\t<div className=\"grid gap-4 sm:grid-cols-2\">\n\t\t\t\t<ConfigItem label={t`Endpoint URL`} value={status.url ?? \"\"} mono />\n\t\t\t\t<ConfigItem label={t`Interval`} value={`${status.interval}s`} />\n\t\t\t\t<ConfigItem label={t`HTTP Method`} value={status.method ?? \"POST\"} />\n\t\t\t</div>\n\n\t\t\t<Separator />\n\n\t\t\t<div>\n\t\t\t\t<h4 className=\"text-base font-medium mb-1\">\n\t\t\t\t\t<Trans>Test heartbeat</Trans>\n\t\t\t\t</h4>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed mb-3\">\n\t\t\t\t\t<Trans>Send a single heartbeat ping to verify your endpoint is working.</Trans>\n\t\t\t\t</p>\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\tclassName=\"flex items-center gap-1.5\"\n\t\t\t\t\tonClick={sendTestHeartbeat}\n\t\t\t\t\tdisabled={isTesting}\n\t\t\t\t>\n\t\t\t\t\t<TestIcon className={cn(\"size-4\", isTesting && \"animate-spin\")} />\n\t\t\t\t\t<Trans>Send test heartbeat</Trans>\n\t\t\t\t</Button>\n\t\t\t</div>\n\n\t\t\t<Separator />\n\n\t\t\t<div>\n\t\t\t\t<h4 className=\"text-base font-medium mb-2\">\n\t\t\t\t\t<Trans>Payload format</Trans>\n\t\t\t\t</h4>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed mb-2\">\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\tWhen using POST, each heartbeat includes a JSON payload with system status summary, list of down systems,\n\t\t\t\t\t\tand triggered alerts.\n\t\t\t\t\t</Trans>\n\t\t\t\t</p>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\tThe overall status is <code className=\"bg-muted rounded-sm px-1 text-primary\">ok</code> when all systems are\n\t\t\t\t\t\tup, <code className=\"bg-muted rounded-sm px-1 text-primary\">warn</code> when alerts are triggered, and{\" \"}\n\t\t\t\t\t\t<code className=\"bg-muted rounded-sm px-1 text-primary\">error</code> when any system is down.\n\t\t\t\t\t</Trans>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nfunction NotEnabledState({ isLoading }: { isLoading?: boolean }) {\n\treturn (\n\t\t<div className={cn(\"grid gap-4\", isLoading && \"animate-pulse\")}>\n\t\t\t<div>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed mb-3\">\n\t\t\t\t\t<Trans>Set the following environment variables on your Beszel hub to enable heartbeat monitoring:</Trans>\n\t\t\t\t</p>\n\t\t\t\t<div className=\"grid gap-2.5\">\n\t\t\t\t\t<EnvVarItem\n\t\t\t\t\t\tname=\"HEARTBEAT_URL\"\n\t\t\t\t\t\tdescription={t`Endpoint URL to ping (required)`}\n\t\t\t\t\t\texample=\"https://uptime.betterstack.com/api/v1/heartbeat/xxxx\"\n\t\t\t\t\t/>\n\t\t\t\t\t<EnvVarItem name=\"HEARTBEAT_INTERVAL\" description={t`Seconds between pings (default: 60)`} example=\"60\" />\n\t\t\t\t\t<EnvVarItem\n\t\t\t\t\t\tname=\"HEARTBEAT_METHOD\"\n\t\t\t\t\t\tdescription={t`HTTP method: POST, GET, or HEAD (default: POST)`}\n\t\t\t\t\t\texample=\"POST\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t<Trans>After setting the environment variables, restart your Beszel hub for changes to take effect.</Trans>\n\t\t\t</p>\n\t\t</div>\n\t)\n}\n\nfunction ConfigItem({ label, value, mono }: { label: string; value: string; mono?: boolean }) {\n\treturn (\n\t\t<div>\n\t\t\t<p className=\"text-sm font-medium mb-0.5\">{label}</p>\n\t\t\t<p className={cn(\"text-sm text-muted-foreground break-all\", mono && \"font-mono\")}>{value}</p>\n\t\t</div>\n\t)\n}\n\nfunction EnvVarItem({ name, description, example }: { name: string; description: string; example: string }) {\n\treturn (\n\t\t<div className=\"bg-muted/50 rounded-md px-3 py-2.5 grid gap-1.5\">\n\t\t\t<code className=\"text-sm font-mono text-primary font-medium leading-tight\">{name}</code>\n\t\t\t<p className=\"text-sm text-muted-foreground\">{description}</p>\n\t\t\t<p className=\"text-xs text-muted-foreground\">\n\t\t\t\t<Trans>Example:</Trans> <code className=\"font-mono\">{example}</code>\n\t\t\t</p>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/layout.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath, redirectPage } from \"@nanostores/router\"\nimport {\n\tAlertOctagonIcon,\n\tBellIcon,\n\tFileSlidersIcon,\n\tFingerprintIcon,\n\tHeartPulseIcon,\n\tSettingsIcon,\n} from \"lucide-react\"\nimport { lazy, useEffect } from \"react\"\nimport { $router } from \"@/components/router.tsx\"\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card.tsx\"\nimport { toast } from \"@/components/ui/use-toast.ts\"\nimport { pb } from \"@/lib/api\"\nimport { $userSettings } from \"@/lib/stores.ts\"\nimport type { UserSettings } from \"@/types\"\nimport { Separator } from \"../../ui/separator\"\nimport { SidebarNav } from \"./sidebar-nav.tsx\"\n\nconst generalSettingsImport = () => import(\"./general.tsx\")\nconst notificationsSettingsImport = () => import(\"./notifications.tsx\")\nconst configYamlSettingsImport = () => import(\"./config-yaml.tsx\")\nconst fingerprintsSettingsImport = () => import(\"./tokens-fingerprints.tsx\")\nconst alertsHistoryDataTableSettingsImport = () => import(\"./alerts-history-data-table.tsx\")\nconst heartbeatSettingsImport = () => import(\"./heartbeat.tsx\")\n\nconst GeneralSettings = lazy(generalSettingsImport)\nconst NotificationsSettings = lazy(notificationsSettingsImport)\nconst ConfigYamlSettings = lazy(configYamlSettingsImport)\nconst FingerprintsSettings = lazy(fingerprintsSettingsImport)\nconst AlertsHistoryDataTableSettings = lazy(alertsHistoryDataTableSettingsImport)\nconst HeartbeatSettings = lazy(heartbeatSettingsImport)\n\nexport async function saveSettings(newSettings: Partial<UserSettings>) {\n\ttry {\n\t\t// get fresh copy of settings\n\t\tconst req = await pb.collection(\"user_settings\").getFirstListItem(\"\", {\n\t\t\tfields: \"id,settings\",\n\t\t})\n\t\t// update user settings\n\t\tconst updatedSettings = await pb.collection(\"user_settings\").update(req.id, {\n\t\t\tsettings: {\n\t\t\t\t...req.settings,\n\t\t\t\t...newSettings,\n\t\t\t},\n\t\t})\n\t\t$userSettings.set(updatedSettings.settings)\n\t\ttoast({\n\t\t\ttitle: t`Settings saved`,\n\t\t\tdescription: t`Your user settings have been updated.`,\n\t\t})\n\t} catch (e) {\n\t\t// console.error('update settings', e)\n\t\ttoast({\n\t\t\ttitle: t`Failed to save settings`,\n\t\t\tdescription: t`Check logs for more details.`,\n\t\t\tvariant: \"destructive\",\n\t\t})\n\t}\n}\n\nexport default function SettingsLayout() {\n\tconst { t } = useLingui()\n\n\tconst sidebarNavItems = [\n\t\t{\n\t\t\ttitle: t({ message: `General`, comment: \"Context: General settings\" }),\n\t\t\thref: getPagePath($router, \"settings\", { name: \"general\" }),\n\t\t\ticon: SettingsIcon,\n\t\t},\n\t\t{\n\t\t\ttitle: t`Notifications`,\n\t\t\thref: getPagePath($router, \"settings\", { name: \"notifications\" }),\n\t\t\ticon: BellIcon,\n\t\t\tpreload: notificationsSettingsImport,\n\t\t},\n\t\t{\n\t\t\ttitle: t`Tokens & Fingerprints`,\n\t\t\thref: getPagePath($router, \"settings\", { name: \"tokens\" }),\n\t\t\ticon: FingerprintIcon,\n\t\t\tnoReadOnly: true,\n\t\t\tpreload: fingerprintsSettingsImport,\n\t\t},\n\t\t{\n\t\t\ttitle: t`Alert History`,\n\t\t\thref: getPagePath($router, \"settings\", { name: \"alert-history\" }),\n\t\t\ticon: AlertOctagonIcon,\n\t\t\tpreload: alertsHistoryDataTableSettingsImport,\n\t\t},\n\t\t{\n\t\t\ttitle: t`Heartbeat`,\n\t\t\thref: getPagePath($router, \"settings\", { name: \"heartbeat\" }),\n\t\t\ticon: HeartPulseIcon,\n\t\t\tadmin: true,\n\t\t\tpreload: heartbeatSettingsImport,\n\t\t},\n\t\t{\n\t\t\ttitle: t`YAML Config`,\n\t\t\thref: getPagePath($router, \"settings\", { name: \"config\" }),\n\t\t\ticon: FileSlidersIcon,\n\t\t\tadmin: true,\n\t\t\tpreload: configYamlSettingsImport,\n\t\t},\n\t]\n\n\tconst page = useStore($router)\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: no dependencies\n\tuseEffect(() => {\n\t\tdocument.title = `${t`Settings`} / Beszel`\n\t\t// @ts-expect-error redirect to account page if no page is specified\n\t\tif (!page?.params?.name) {\n\t\t\tredirectPage($router, \"settings\", { name: \"general\" })\n\t\t}\n\t}, [])\n\n\treturn (\n\t\t<Card className=\"pt-5 px-4 pb-8 min-h-96 mb-14 sm:pt-6 sm:px-7\">\n\t\t\t<CardHeader className=\"p-0\">\n\t\t\t\t<CardTitle className=\"mb-1\">\n\t\t\t\t\t<Trans>Settings</Trans>\n\t\t\t\t</CardTitle>\n\t\t\t\t<CardDescription>\n\t\t\t\t\t<Trans>Manage display and notification preferences.</Trans>\n\t\t\t\t</CardDescription>\n\t\t\t</CardHeader>\n\t\t\t<CardContent className=\"p-0\">\n\t\t\t\t<Separator className=\"hidden md:block my-5\" />\n\t\t\t\t<div className=\"flex flex-col gap-3.5 md:flex-row md:gap-5 lg:gap-12\">\n\t\t\t\t\t<aside className=\"md:max-w-52 min-w-40\">\n\t\t\t\t\t\t<SidebarNav items={sidebarNavItems} />\n\t\t\t\t\t</aside>\n\t\t\t\t\t<div className=\"flex-1 min-w-0\">\n\t\t\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t\t\t<SettingsContent name={page?.params?.name ?? \"general\"} />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</CardContent>\n\t\t</Card>\n\t)\n}\n\nfunction SettingsContent({ name }: { name: string }) {\n\tconst userSettings = useStore($userSettings)\n\n\tswitch (name) {\n\t\tcase \"general\":\n\t\t\treturn <GeneralSettings userSettings={userSettings} />\n\t\tcase \"notifications\":\n\t\t\treturn <NotificationsSettings userSettings={userSettings} />\n\t\tcase \"config\":\n\t\t\treturn <ConfigYamlSettings />\n\t\tcase \"tokens\":\n\t\t\treturn <FingerprintsSettings />\n\t\tcase \"alert-history\":\n\t\t\treturn <AlertsHistoryDataTableSettings />\n\t\tcase \"heartbeat\":\n\t\t\treturn <HeartbeatSettings />\n\t}\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/notifications.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from \"lucide-react\"\nimport { type ChangeEventHandler, useEffect, useState } from \"react\"\nimport * as v from \"valibot\"\nimport { prependBasePath } from \"@/components/router\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card } from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { InputTags } from \"@/components/ui/input-tags\"\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport { isAdmin, pb } from \"@/lib/api\"\nimport type { UserSettings } from \"@/types\"\nimport { saveSettings } from \"./layout\"\nimport { QuietHours } from \"./quiet-hours\"\n\ninterface ShoutrrrUrlCardProps {\n\turl: string\n\tonUrlChange: ChangeEventHandler<HTMLInputElement>\n\tonRemove: () => void\n}\n\nconst NotificationSchema = v.object({\n\temails: v.array(v.pipe(v.string(), v.email())),\n\twebhooks: v.array(v.pipe(v.string(), v.url())),\n})\n\nconst SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSettings }) => {\n\tconst [webhooks, setWebhooks] = useState(userSettings.webhooks ?? [])\n\tconst [emails, setEmails] = useState<string[]>(userSettings.emails ?? [])\n\tconst [isLoading, setIsLoading] = useState(false)\n\n\t// update values when userSettings changes\n\tuseEffect(() => {\n\t\tsetWebhooks(userSettings.webhooks ?? [])\n\t\tsetEmails(userSettings.emails ?? [])\n\t}, [userSettings])\n\n\tfunction addWebhook() {\n\t\tsetWebhooks([...webhooks, \"\"])\n\t\t// focus on the new input\n\t\tqueueMicrotask(() => {\n\t\t\tconst inputs = document.querySelectorAll(\"#webhooks input\") as NodeListOf<HTMLInputElement>\n\t\t\tinputs[inputs.length - 1]?.focus()\n\t\t})\n\t}\n\tconst removeWebhook = (index: number) => setWebhooks(webhooks.filter((_, i) => i !== index))\n\n\tfunction updateWebhook(index: number, value: string) {\n\t\tconst newWebhooks = [...webhooks]\n\t\tnewWebhooks[index] = value\n\t\tsetWebhooks(newWebhooks)\n\t}\n\n\tasync function updateSettings() {\n\t\tsetIsLoading(true)\n\t\ttry {\n\t\t\tconst parsedData = v.parse(NotificationSchema, { emails, webhooks })\n\t\t\tawait saveSettings(parsedData)\n\t\t} catch (e: any) {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Failed to save settings`,\n\t\t\t\tdescription: e.message,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t}\n\t\tsetIsLoading(false)\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<div>\n\t\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t\t<Trans>Notifications</Trans>\n\t\t\t\t</h3>\n\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t<Trans>Configure how you receive alert notifications.</Trans>\n\t\t\t\t</p>\n\t\t\t\t<p className=\"text-sm text-muted-foreground mt-1.5 leading-relaxed\">\n\t\t\t\t\t<Trans>\n\t\t\t\t\t\tLooking instead for where to create alerts? Click the bell <BellIcon className=\"inline h-4 w-4\" /> icons in\n\t\t\t\t\t\tthe systems table.\n\t\t\t\t\t</Trans>\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t\t<Separator className=\"my-4\" />\n\t\t\t<div className=\"space-y-5\">\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<div className=\"mb-2\">\n\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t<Trans>Email notifications</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t{isAdmin() && (\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tPlease{\" \"}\n\t\t\t\t\t\t\t\t\t<a href={prependBasePath(\"/_/#/settings/mail\")} className=\"link\" target=\"_blank\">\n\t\t\t\t\t\t\t\t\t\tconfigure an SMTP server\n\t\t\t\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\t\t\t\tto ensure alerts are delivered.\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t<Label className=\"block\" htmlFor=\"email\">\n\t\t\t\t\t\t<Trans>To email(s)</Trans>\n\t\t\t\t\t</Label>\n\t\t\t\t\t<InputTags\n\t\t\t\t\t\tvalue={emails}\n\t\t\t\t\t\tonChange={setEmails}\n\t\t\t\t\t\tplaceholder={t`Enter email address...`}\n\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t\ttype=\"email\"\n\t\t\t\t\t\tid=\"email\"\n\t\t\t\t\t/>\n\t\t\t\t\t<p className=\"text-[0.8rem] text-muted-foreground\">\n\t\t\t\t\t\t<Trans>Save address using enter key or comma. Leave blank to disable email notifications.</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"space-y-3\">\n\t\t\t\t\t<div className=\"grid grid-cols-1 sm:flex items-center justify-between gap-4\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">\n\t\t\t\t\t\t\t\t<Trans>Webhook / Push notifications</Trans>\n\t\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tBeszel uses{\" \"}\n\t\t\t\t\t\t\t\t\t<a href=\"https://beszel.dev/guide/notifications\" target=\"_blank\" className=\"link\" rel=\"noopener\">\n\t\t\t\t\t\t\t\t\t\tShoutrrr\n\t\t\t\t\t\t\t\t\t</a>{\" \"}\n\t\t\t\t\t\t\t\t\tto integrate with popular notification services.\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tclassName=\"h-10 shrink-0\"\n\t\t\t\t\t\t\tonClick={addWebhook}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<PlusIcon className=\"size-4\" />\n\t\t\t\t\t\t\t<span className=\"ms-1\">\n\t\t\t\t\t\t\t\t<Trans>Add URL</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t\t{webhooks.length > 0 && (\n\t\t\t\t\t\t<div className=\"grid gap-2.5\" id=\"webhooks\">\n\t\t\t\t\t\t\t{webhooks.map((webhook, index) => (\n\t\t\t\t\t\t\t\t<ShoutrrrUrlCard\n\t\t\t\t\t\t\t\t\tkey={index}\n\t\t\t\t\t\t\t\t\turl={webhook}\n\t\t\t\t\t\t\t\t\tonUrlChange={(e: React.ChangeEvent<HTMLInputElement>) => updateWebhook(index, e.target.value)}\n\t\t\t\t\t\t\t\t\tonRemove={() => removeWebhook(index)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<div className=\"space-y-3\">\n\t\t\t\t\t<QuietHours />\n\t\t\t\t</div>\n\t\t\t\t<Separator />\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"flex items-center gap-1.5 disabled:opacity-100\"\n\t\t\t\t\tonClick={updateSettings}\n\t\t\t\t\tdisabled={isLoading}\n\t\t\t\t>\n\t\t\t\t\t{isLoading ? <LoaderCircleIcon className=\"h-4 w-4 animate-spin\" /> : <SaveIcon className=\"h-4 w-4\" />}\n\t\t\t\t\t<Trans>Save Settings</Trans>\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\nconst ShoutrrrUrlCard = ({ url, onUrlChange, onRemove }: ShoutrrrUrlCardProps) => {\n\tconst [isLoading, setIsLoading] = useState(false)\n\n\tconst sendTestNotification = async () => {\n\t\tsetIsLoading(true)\n\t\tconst res = await pb.send(\"/api/beszel/test-notification\", { method: \"POST\", body: { url } })\n\t\tif (\"err\" in res && !res.err) {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Test notification sent`,\n\t\t\t\tdescription: t`Check your notification service`,\n\t\t\t})\n\t\t} else {\n\t\t\ttoast({\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: res.err ?? t`Failed to send test notification`,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t}\n\t\tsetIsLoading(false)\n\t}\n\n\treturn (\n\t\t<Card className=\"bg-table-header p-2 md:p-3\">\n\t\t\t<div className=\"flex items-center gap-1\">\n\t\t\t\t<Input\n\t\t\t\t\ttype=\"url\"\n\t\t\t\t\tclassName=\"light:bg-card\"\n\t\t\t\t\trequired\n\t\t\t\t\tplaceholder=\"generic://webhook.site/xxxxxx\"\n\t\t\t\t\tvalue={url}\n\t\t\t\t\tonChange={onUrlChange}\n\t\t\t\t/>\n\t\t\t\t<Button type=\"button\" variant=\"outline\" disabled={isLoading || url === \"\"} onClick={sendTestNotification}>\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t<LoaderCircleIcon className=\"h-4 w-4 animate-spin\" />\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\tTest <span className=\"hidden sm:inline\">URL</span>\n\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t)}\n\t\t\t\t</Button>\n\t\t\t\t<Button type=\"button\" variant=\"outline\" size=\"icon\" className=\"shrink-0\" aria-label=\"Delete\" onClick={onRemove}>\n\t\t\t\t\t<Trash2Icon className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n\nexport default SettingsNotificationsPage\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/quiet-hours.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport {\n\tMoreHorizontalIcon,\n\tPlusIcon,\n\tTrash2Icon,\n\tServerIcon,\n\tClockIcon,\n\tCalendarIcon,\n\tActivityIcon,\n\tPenSquareIcon,\n} from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n\tDialog,\n\tDialogContent,\n\tDialogDescription,\n\tDialogFooter,\n\tDialogHeader,\n\tDialogTitle,\n\tDialogTrigger,\n} from \"@/components/ui/dialog\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { useToast } from \"@/components/ui/use-toast\"\nimport { pb } from \"@/lib/api\"\nimport { $systems } from \"@/lib/stores\"\nimport { formatShortDate } from \"@/lib/utils\"\nimport type { QuietHoursRecord, SystemRecord } from \"@/types\"\n\nconst quietHoursTranslation = t`Quiet Hours`\n\nexport function QuietHours() {\n\tconst [data, setData] = useState<QuietHoursRecord[]>([])\n\tconst [dialogOpen, setDialogOpen] = useState(false)\n\tconst [editingRecord, setEditingRecord] = useState<QuietHoursRecord | null>(null)\n\tconst { toast } = useToast()\n\tconst systems = useStore($systems)\n\tuseEffect(() => {\n\t\tlet unsubscribe: (() => void) | undefined\n\t\tconst pbOptions = {\n\t\t\texpand: \"system\",\n\t\t\tfields: \"id,user,system,type,start,end,expand.system.name\",\n\t\t}\n\t\t// Initial load\n\t\tpb.collection<QuietHoursRecord>(\"quiet_hours\")\n\t\t\t.getList(0, 200, {\n\t\t\t\t...pbOptions,\n\t\t\t\tsort: \"system\",\n\t\t\t})\n\t\t\t.then(({ items }) => setData(items))\n\n\t\t// Subscribe to changes\n\t\t;(async () => {\n\t\t\tunsubscribe = await pb.collection(\"quiet_hours\").subscribe(\n\t\t\t\t\"*\",\n\t\t\t\t(e) => {\n\t\t\t\t\tif (e.action === \"create\") {\n\t\t\t\t\t\tsetData((current) => [e.record as QuietHoursRecord, ...current])\n\t\t\t\t\t}\n\t\t\t\t\tif (e.action === \"update\") {\n\t\t\t\t\t\tsetData((current) => current.map((r) => (r.id === e.record.id ? (e.record as QuietHoursRecord) : r)))\n\t\t\t\t\t}\n\t\t\t\t\tif (e.action === \"delete\") {\n\t\t\t\t\t\tsetData((current) => current.filter((r) => r.id !== e.record.id))\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tpbOptions\n\t\t\t)\n\t\t})()\n\t\t// Unsubscribe on unmount\n\t\treturn () => unsubscribe?.()\n\t}, [])\n\n\tconst handleDelete = async (id: string) => {\n\t\ttry {\n\t\t\tawait pb.collection(\"quiet_hours\").delete(id)\n\t\t} catch (e: unknown) {\n\t\t\ttoast({\n\t\t\t\tvariant: \"destructive\",\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: (e as Error).message || \"Failed to delete quiet hours.\",\n\t\t\t})\n\t\t}\n\t}\n\n\tconst openEditDialog = (record: QuietHoursRecord) => {\n\t\tsetEditingRecord(record)\n\t\tsetDialogOpen(true)\n\t}\n\n\tconst closeDialog = () => {\n\t\tsetDialogOpen(false)\n\t\tsetEditingRecord(null)\n\t}\n\n\tconst formatDateTime = (record: QuietHoursRecord) => {\n\t\tif (record.type === \"daily\") {\n\t\t\t// For daily windows, show only time\n\t\t\tconst startTime = new Date(record.start).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })\n\t\t\tconst endTime = new Date(record.end).toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" })\n\t\t\treturn `${startTime} - ${endTime}`\n\t\t}\n\t\t// For one-time windows, show full date and time\n\t\tconst start = formatShortDate(record.start)\n\t\tconst end = formatShortDate(record.end)\n\t\treturn `${start} - ${end}`\n\t}\n\n\tconst getWindowState = (record: QuietHoursRecord): \"active\" | \"past\" | \"inactive\" => {\n\t\tconst now = new Date()\n\n\t\tif (record.type === \"daily\") {\n\t\t\t// For daily windows, check if current time is within the window\n\t\t\tconst startDate = new Date(record.start)\n\t\t\tconst endDate = new Date(record.end)\n\n\t\t\t// Get current time in local timezone\n\t\t\tconst currentMinutes = now.getHours() * 60 + now.getMinutes()\n\t\t\tconst startMinutes = startDate.getUTCHours() * 60 + startDate.getUTCMinutes()\n\t\t\tconst endMinutes = endDate.getUTCHours() * 60 + endDate.getUTCMinutes()\n\n\t\t\t// Convert UTC to local time offset\n\t\t\tconst offset = now.getTimezoneOffset()\n\t\t\tconst localStartMinutes = (startMinutes - offset + 1440) % 1440\n\t\t\tconst localEndMinutes = (endMinutes - offset + 1440) % 1440\n\n\t\t\t// Handle cases where window spans midnight\n\t\t\tif (localStartMinutes <= localEndMinutes) {\n\t\t\t\treturn currentMinutes >= localStartMinutes && currentMinutes < localEndMinutes ? \"active\" : \"inactive\"\n\t\t\t} else {\n\t\t\t\treturn currentMinutes >= localStartMinutes || currentMinutes < localEndMinutes ? \"active\" : \"inactive\"\n\t\t\t}\n\t\t} else {\n\t\t\t// For one-time windows\n\t\t\tconst startDate = new Date(record.start)\n\t\t\tconst endDate = new Date(record.end)\n\n\t\t\tif (now >= startDate && now < endDate) {\n\t\t\t\treturn \"active\"\n\t\t\t} else if (now >= endDate) {\n\t\t\t\treturn \"past\"\n\t\t\t} else {\n\t\t\t\treturn \"inactive\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"grid grid-cols-1 sm:flex items-center justify-between gap-4 mb-3\">\n\t\t\t\t<div>\n\t\t\t\t\t<h3 className=\"mb-1 text-lg font-medium\">{quietHoursTranslation}</h3>\n\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\tSchedule quiet hours where notifications will not be sent, such as during maintenance periods.\n\t\t\t\t\t\t</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n\t\t\t\t\t<DialogTrigger asChild>\n\t\t\t\t\t\t<Button variant=\"outline\" className=\"h-10 shrink-0\" onClick={() => setEditingRecord(null)}>\n\t\t\t\t\t\t\t<PlusIcon className=\"size-4\" />\n\t\t\t\t\t\t\t<span className=\"ms-1\">\n\t\t\t\t\t\t\t\t<Trans>Add {{ foo: quietHoursTranslation }}</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</DialogTrigger>\n\t\t\t\t\t<QuietHoursDialog editingRecord={editingRecord} systems={systems} onClose={closeDialog} toast={toast} />\n\t\t\t\t</Dialog>\n\t\t\t</div>\n\t\t\t{data.length > 0 && (\n\t\t\t\t<div className=\"rounded-md border overflow-x-auto whitespace-nowrap\">\n\t\t\t\t\t<Table>\n\t\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t\t<TableRow className=\"border-border/50\">\n\t\t\t\t\t\t\t\t<TableHead className=\"px-4\">\n\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<ServerIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t<Trans>System</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t<TableHead className=\"px-4\">\n\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<ClockIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t<Trans>Type</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t<TableHead className=\"px-4\">\n\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<CalendarIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t<Trans>Schedule</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t<TableHead className=\"px-4\">\n\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t<ActivityIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t<Trans>State</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t<TableHead className=\"px-4 text-right sr-only\">\n\t\t\t\t\t\t\t\t\t<Trans>Actions</Trans>\n\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t</TableHeader>\n\t\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t\t{data.map((record) => (\n\t\t\t\t\t\t\t\t<TableRow key={record.id}>\n\t\t\t\t\t\t\t\t\t<TableCell className=\"px-4 py-3\">\n\t\t\t\t\t\t\t\t\t\t{record.system ? record.expand?.system?.name || record.system : <Trans>All Systems</Trans>}\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell className=\"px-4 py-3\">\n\t\t\t\t\t\t\t\t\t\t{record.type === \"daily\" ? <Trans>Daily</Trans> : <Trans>One-time</Trans>}\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell className=\"px-4 py-3\">{formatDateTime(record)}</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell className=\"px-4 py-3\">\n\t\t\t\t\t\t\t\t\t\t{(() => {\n\t\t\t\t\t\t\t\t\t\t\tconst state = getWindowState(record)\n\t\t\t\t\t\t\t\t\t\t\tconst stateConfig = {\n\t\t\t\t\t\t\t\t\t\t\t\tactive: { label: <Trans>Active</Trans>, variant: \"success\" as const },\n\t\t\t\t\t\t\t\t\t\t\t\tpast: { label: <Trans>Past</Trans>, variant: \"danger\" as const },\n\t\t\t\t\t\t\t\t\t\t\t\tinactive: { label: <Trans>Inactive</Trans>, variant: \"default\" as const },\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tconst config = stateConfig[state]\n\t\t\t\t\t\t\t\t\t\t\treturn <Badge variant={config.variant}>{config.label}</Badge>\n\t\t\t\t\t\t\t\t\t\t})()}\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t<TableCell className=\"px-4 py-3 text-right\">\n\t\t\t\t\t\t\t\t\t\t<DropdownMenu>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t\t\t<Button variant=\"ghost\" size=\"icon\" className=\"size-8\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Open menu</Trans>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<MoreHorizontalIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuContent align=\"end\">\n\t\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuItem onClick={() => openEditDialog(record)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<PenSquareIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Edit</Trans>\n\t\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuItem onClick={() => handleDelete(record.id)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Trash2Icon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Delete</Trans>\n\t\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuContent>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenu>\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</TableBody>\n\t\t\t\t\t</Table>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</>\n\t)\n}\n\n// Helper function to format Date as datetime-local string (YYYY-MM-DDTHH:mm) in local time\nfunction formatDateTimeLocal(date: Date): string {\n\tconst year = date.getFullYear()\n\tconst month = String(date.getMonth() + 1).padStart(2, \"0\")\n\tconst day = String(date.getDate()).padStart(2, \"0\")\n\tconst hours = String(date.getHours()).padStart(2, \"0\")\n\tconst minutes = String(date.getMinutes()).padStart(2, \"0\")\n\treturn `${year}-${month}-${day}T${hours}:${minutes}`\n}\n\nfunction QuietHoursDialog({\n\teditingRecord,\n\tsystems,\n\tonClose,\n\ttoast,\n}: {\n\teditingRecord: QuietHoursRecord | null\n\tsystems: SystemRecord[]\n\tonClose: () => void\n\ttoast: ReturnType<typeof useToast>[\"toast\"]\n}) {\n\tconst [selectedSystem, setSelectedSystem] = useState(editingRecord?.system || \"\")\n\tconst [isGlobal, setIsGlobal] = useState(!editingRecord?.system)\n\tconst [windowType, setWindowType] = useState<\"one-time\" | \"daily\">(editingRecord?.type || \"one-time\")\n\tconst [startDateTime, setStartDateTime] = useState(\"\")\n\tconst [endDateTime, setEndDateTime] = useState(\"\")\n\tconst [startTime, setStartTime] = useState(\"\")\n\tconst [endTime, setEndTime] = useState(\"\")\n\n\tuseEffect(() => {\n\t\tif (editingRecord) {\n\t\t\tsetSelectedSystem(editingRecord.system || \"\")\n\t\t\tsetIsGlobal(!editingRecord.system)\n\t\t\tsetWindowType(editingRecord.type)\n\t\t\tif (editingRecord.type === \"daily\") {\n\t\t\t\t// Extract time from datetime\n\t\t\t\tconst start = new Date(editingRecord.start)\n\t\t\t\tconst end = editingRecord.end ? new Date(editingRecord.end) : null\n\t\t\t\tsetStartTime(start.toTimeString().slice(0, 5))\n\t\t\t\tsetEndTime(end ? end.toTimeString().slice(0, 5) : \"\")\n\t\t\t} else {\n\t\t\t\t// For one-time, format as datetime-local (local time, not UTC)\n\t\t\t\tconst startDate = new Date(editingRecord.start)\n\t\t\t\tconst endDate = editingRecord.end ? new Date(editingRecord.end) : null\n\n\t\t\t\tsetStartDateTime(formatDateTimeLocal(startDate))\n\t\t\t\tsetEndDateTime(endDate ? formatDateTimeLocal(endDate) : \"\")\n\t\t\t}\n\t\t} else {\n\t\t\t// Reset form with default dates: today at 12pm and 1pm\n\t\t\tconst today = new Date()\n\t\t\tconst noon = new Date(today)\n\t\t\tnoon.setHours(12, 0, 0, 0)\n\t\t\tconst onePm = new Date(today)\n\t\t\tonePm.setHours(13, 0, 0, 0)\n\n\t\t\tsetSelectedSystem(\"\")\n\t\t\tsetIsGlobal(true)\n\t\t\tsetWindowType(\"one-time\")\n\t\t\tsetStartDateTime(formatDateTimeLocal(noon))\n\t\t\tsetEndDateTime(formatDateTimeLocal(onePm))\n\t\t\tsetStartTime(\"12:00\")\n\t\t\tsetEndTime(\"13:00\")\n\t\t}\n\t}, [editingRecord])\n\n\tconst handleSubmit = async (e: React.FormEvent) => {\n\t\te.preventDefault()\n\n\t\ttry {\n\t\t\tlet startValue: string\n\t\t\tlet endValue: string | undefined\n\n\t\t\tif (windowType === \"daily\") {\n\t\t\t\t// For daily windows, convert local time to UTC\n\t\t\t\t// Create a date with the time in local timezone, then convert to UTC\n\t\t\t\tconst startDate = new Date(`2000-01-01T${startTime}:00`)\n\t\t\t\tstartValue = startDate.toISOString()\n\n\t\t\t\tif (endTime) {\n\t\t\t\t\tconst endDate = new Date(`2000-01-01T${endTime}:00`)\n\t\t\t\t\tendValue = endDate.toISOString()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// For one-time windows, use the datetime values\n\t\t\t\tstartValue = new Date(startDateTime).toISOString()\n\t\t\t\tendValue = endDateTime ? new Date(endDateTime).toISOString() : undefined\n\t\t\t}\n\n\t\t\tconst data = {\n\t\t\t\tuser: pb.authStore.record?.id,\n\t\t\t\tsystem: isGlobal ? undefined : selectedSystem,\n\t\t\t\ttype: windowType,\n\t\t\t\tstart: startValue,\n\t\t\t\tend: endValue,\n\t\t\t}\n\n\t\t\tif (editingRecord) {\n\t\t\t\tawait pb.collection(\"quiet_hours\").update(editingRecord.id, data)\n\t\t\t} else {\n\t\t\t\tawait pb.collection(\"quiet_hours\").create(data)\n\t\t\t}\n\n\t\t\tonClose()\n\t\t} catch (e) {\n\t\t\ttoast({\n\t\t\t\tvariant: \"destructive\",\n\t\t\t\ttitle: t`Error`,\n\t\t\t\tdescription: t`Failed to save settings`,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn (\n\t\t<DialogContent>\n\t\t\t<DialogHeader>\n\t\t\t\t<DialogTitle>\n\t\t\t\t\t{editingRecord ? (\n\t\t\t\t\t\t<Trans>Edit {{ foo: quietHoursTranslation }}</Trans>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<Trans>Add {{ foo: quietHoursTranslation }}</Trans>\n\t\t\t\t\t)}\n\t\t\t\t</DialogTitle>\n\t\t\t\t<DialogDescription>\n\t\t\t\t\t<Trans>Schedule quiet hours where notifications will not be sent.</Trans>\n\t\t\t\t</DialogDescription>\n\t\t\t</DialogHeader>\n\t\t\t<form onSubmit={handleSubmit} className=\"space-y-4\">\n\t\t\t\t<Tabs value={isGlobal ? \"global\" : \"system\"} onValueChange={(value) => setIsGlobal(value === \"global\")}>\n\t\t\t\t\t<TabsList className=\"grid w-full grid-cols-2\">\n\t\t\t\t\t\t<TabsTrigger value=\"global\">\n\t\t\t\t\t\t\t<Trans>Global</Trans>\n\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t\t<TabsTrigger value=\"system\">\n\t\t\t\t\t\t\t<Trans>System</Trans>\n\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t</TabsList>\n\n\t\t\t\t\t<TabsContent value=\"system\" className=\"mt-4 space-y-4\">\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label htmlFor=\"system\">\n\t\t\t\t\t\t\t\t<Trans>System</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Select value={selectedSystem} onValueChange={setSelectedSystem}>\n\t\t\t\t\t\t\t\t<SelectTrigger id=\"system\">\n\t\t\t\t\t\t\t\t\t<SelectValue placeholder={t`Select ${{ foo: t`System`.toLocaleLowerCase() }}`} />\n\t\t\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t\t\t{systems.map((system) => (\n\t\t\t\t\t\t\t\t\t\t<SelectItem key={system.id} value={system.id}>\n\t\t\t\t\t\t\t\t\t\t\t{system.name}\n\t\t\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t\t\t</Select>\n\t\t\t\t\t\t\t{/* Hidden input for native form validation */}\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tclassName=\"sr-only\"\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\tautoComplete=\"off\"\n\t\t\t\t\t\t\t\tvalue={selectedSystem}\n\t\t\t\t\t\t\t\tonChange={() => {}}\n\t\t\t\t\t\t\t\trequired={!isGlobal}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</TabsContent>\n\t\t\t\t</Tabs>\n\n\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t<Label htmlFor=\"type\">\n\t\t\t\t\t\t<Trans>Type</Trans>\n\t\t\t\t\t</Label>\n\t\t\t\t\t<Select value={windowType} onValueChange={(value: \"one-time\" | \"daily\") => setWindowType(value)}>\n\t\t\t\t\t\t<SelectTrigger id=\"type\">\n\t\t\t\t\t\t\t<SelectValue />\n\t\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t\t<SelectItem value=\"one-time\">\n\t\t\t\t\t\t\t\t<Trans>One-time</Trans>\n\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t<SelectItem value=\"daily\">\n\t\t\t\t\t\t\t\t<Trans>Daily</Trans>\n\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t</SelectContent>\n\t\t\t\t\t</Select>\n\t\t\t\t</div>\n\n\t\t\t\t{windowType === \"one-time\" ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label htmlFor=\"start-datetime\">\n\t\t\t\t\t\t\t\t<Trans>Start Time</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tid=\"start-datetime\"\n\t\t\t\t\t\t\t\ttype=\"datetime-local\"\n\t\t\t\t\t\t\t\tvalue={startDateTime}\n\t\t\t\t\t\t\t\tonChange={(e) => setStartDateTime(e.target.value)}\n\t\t\t\t\t\t\t\tmin={formatDateTimeLocal(new Date(new Date().setHours(0, 0, 0, 0)))}\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\tclassName=\"tabular-nums tracking-tighter\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"grid gap-2\">\n\t\t\t\t\t\t\t<Label htmlFor=\"end-datetime\">\n\t\t\t\t\t\t\t\t<Trans>End Time</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tid=\"end-datetime\"\n\t\t\t\t\t\t\t\ttype=\"datetime-local\"\n\t\t\t\t\t\t\t\tvalue={endDateTime}\n\t\t\t\t\t\t\t\tonChange={(e) => setEndDateTime(e.target.value)}\n\t\t\t\t\t\t\t\tmin={startDateTime || formatDateTimeLocal(new Date())}\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t\tclassName=\"tabular-nums tracking-tighter\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"grid gap-2 grid-cols-2\">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<Label htmlFor=\"start-time\">\n\t\t\t\t\t\t\t\t<Trans>Start Time</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tclassName=\"tabular-nums tracking-tighter\"\n\t\t\t\t\t\t\t\tid=\"start-time\"\n\t\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\t\tvalue={startTime}\n\t\t\t\t\t\t\t\tonChange={(e) => setStartTime(e.target.value)}\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<Label htmlFor=\"end-time\">\n\t\t\t\t\t\t\t\t<Trans>End Time</Trans>\n\t\t\t\t\t\t\t</Label>\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tclassName=\"tabular-nums tracking-tighter\"\n\t\t\t\t\t\t\t\tid=\"end-time\"\n\t\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\t\tvalue={endTime}\n\t\t\t\t\t\t\t\tonChange={(e) => setEndTime(e.target.value)}\n\t\t\t\t\t\t\t\trequired\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t<DialogFooter>\n\t\t\t\t\t<Button type=\"button\" variant=\"outline\" onClick={onClose}>\n\t\t\t\t\t\t<Trans>Cancel</Trans>\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button type=\"submit\">{editingRecord ? <Trans>Update</Trans> : <Trans>Create</Trans>}</Button>\n\t\t\t\t</DialogFooter>\n\t\t\t</form>\n\t\t</DialogContent>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/sidebar-nav.tsx",
    "content": "import { useStore } from \"@nanostores/react\"\nimport type React from \"react\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { isAdmin, isReadOnlyUser } from \"@/lib/api\"\nimport { cn } from \"@/lib/utils\"\nimport { $router, Link, navigate } from \"../../router\"\nimport { buttonVariants } from \"../../ui/button\"\n\ninterface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {\n\titems: {\n\t\thref: string\n\t\ttitle: string\n\t\ticon?: React.FC<React.SVGProps<SVGSVGElement>>\n\t\tadmin?: boolean\n\t\tnoReadOnly?: boolean\n\t\tpreload?: () => Promise<{ default: React.ComponentType<any> }>\n\t}[]\n}\n\nexport function SidebarNav({ className, items, ...props }: SidebarNavProps) {\n\tconst page = useStore($router)\n\n\treturn (\n\t\t<>\n\t\t\t{/* Mobile View */}\n\t\t\t<div className=\"md:hidden\">\n\t\t\t\t<Select onValueChange={navigate} value={page?.path}>\n\t\t\t\t\t<SelectTrigger className=\"w-full my-3.5\">\n\t\t\t\t\t\t<SelectValue placeholder=\"Select page\" />\n\t\t\t\t\t</SelectTrigger>\n\t\t\t\t\t<SelectContent>\n\t\t\t\t\t\t{items.map((item) => {\n\t\t\t\t\t\t\tif (item.admin && !isAdmin()) return null\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<SelectItem key={item.href} value={item.href}>\n\t\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2 truncate\">\n\t\t\t\t\t\t\t\t\t\t{item.icon && <item.icon className=\"size-4\" />}\n\t\t\t\t\t\t\t\t\t\t<span className=\"truncate\">{item.title}</span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</SelectItem>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t</SelectContent>\n\t\t\t\t</Select>\n\t\t\t\t<Separator />\n\t\t\t</div>\n\n\t\t\t{/* Desktop View */}\n\t\t\t<nav className={cn(\"hidden md:grid gap-1 sticky top-6\", className)} {...props}>\n\t\t\t\t{items.map((item) => {\n\t\t\t\t\tif ((item.admin && !isAdmin()) || (item.noReadOnly && isReadOnlyUser())) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\tonMouseEnter={() => item.preload?.()}\n\t\t\t\t\t\t\tkey={item.href}\n\t\t\t\t\t\t\thref={item.href}\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\tbuttonVariants({ variant: \"ghost\" }),\n\t\t\t\t\t\t\t\t\"flex items-center gap-3 justify-start truncate duration-50\",\n\t\t\t\t\t\t\t\tpage?.path === item.href ? \"bg-muted hover:bg-accent/70\" : \"hover:bg-accent/50\"\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.icon && <item.icon className=\"size-4 shrink-0\" />}\n\t\t\t\t\t\t\t<span className=\"truncate\">{item.title}</span>\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t</nav>\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/settings/tokens-fingerprints.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { redirectPage } from \"@nanostores/router\"\nimport {\n\tCopyIcon,\n\tExternalLinkIcon,\n\tFingerprintIcon,\n\tKeyIcon,\n\tMoreHorizontalIcon,\n\tRotateCwIcon,\n\tServerIcon,\n\tTrash2Icon,\n} from \"lucide-react\"\nimport { memo, useEffect, useMemo, useState } from \"react\"\nimport {\n\tcopyDockerCompose,\n\tcopyDockerRun,\n\tcopyLinuxCommand,\n\tcopyWindowsCommand,\n\ttype DropdownItem,\n\tInstallDropdown,\n} from \"@/components/install-dropdowns\"\nimport { $router } from \"@/components/router\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from \"@/components/ui/icons\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Switch } from \"@/components/ui/switch\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport { isReadOnlyUser, pb } from \"@/lib/api\"\nimport { $publicKey } from \"@/lib/stores\"\nimport { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from \"@/lib/utils\"\nimport type { FingerprintRecord } from \"@/types\"\n\nconst pbFingerprintOptions = {\n\texpand: \"system\",\n\tfields: \"id,fingerprint,token,system,expand.system.name\",\n}\n\nfunction sortFingerprints(fingerprints: FingerprintRecord[]) {\n\treturn fingerprints.sort((a, b) => a.expand.system.name.localeCompare(b.expand.system.name))\n}\n\nconst SettingsFingerprintsPage = memo(() => {\n\tif (isReadOnlyUser()) {\n\t\tredirectPage($router, \"settings\", { name: \"general\" })\n\t}\n\tconst [fingerprints, setFingerprints] = useState<FingerprintRecord[]>([])\n\n\t// Get fingerprint records on mount\n\tuseEffect(() => {\n\t\tpb.collection(\"fingerprints\")\n\t\t\t.getFullList<FingerprintRecord>(pbFingerprintOptions)\n\t\t\t.then((prints) => {\n\t\t\t\tsetFingerprints(sortFingerprints(prints))\n\t\t\t})\n\t}, [])\n\n\t// Subscribe to fingerprint updates\n\tuseEffect(() => {\n\t\tlet unsubscribe: (() => void) | undefined\n\t\t;(async () => {\n\t\t\t// subscribe to fingerprint updates\n\t\t\tunsubscribe = await pb.collection(\"fingerprints\").subscribe(\n\t\t\t\t\"*\",\n\t\t\t\t(res) => {\n\t\t\t\t\tsetFingerprints((currentFingerprints) => {\n\t\t\t\t\t\tif (res.action === \"create\") {\n\t\t\t\t\t\t\treturn sortFingerprints([...currentFingerprints, res.record as FingerprintRecord])\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (res.action === \"update\") {\n\t\t\t\t\t\t\treturn currentFingerprints.map((fingerprint) => {\n\t\t\t\t\t\t\t\tif (fingerprint.id === res.record.id) {\n\t\t\t\t\t\t\t\t\treturn { ...fingerprint, ...res.record } as FingerprintRecord\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn fingerprint\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (res.action === \"delete\") {\n\t\t\t\t\t\t\treturn currentFingerprints.filter((fingerprint) => fingerprint.id !== res.record.id)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn currentFingerprints\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tpbFingerprintOptions\n\t\t\t)\n\t\t})()\n\t\t// unsubscribe on unmount\n\t\treturn () => unsubscribe?.()\n\t}, [])\n\n\t// Update token map whenever fingerprints change\n\tuseEffect(() => {\n\t\tfor (const fingerprint of fingerprints) {\n\t\t\ttokenMap.set(fingerprint.system, fingerprint.token)\n\t\t}\n\t}, [fingerprints])\n\n\treturn (\n\t\t<>\n\t\t\t<SectionIntro />\n\t\t\t<Separator className=\"my-4\" />\n\t\t\t<SectionUniversalToken />\n\t\t\t<Separator className=\"my-4\" />\n\t\t\t<SectionTable fingerprints={fingerprints} />\n\t\t</>\n\t)\n})\n\nconst SectionIntro = memo(() => {\n\treturn (\n\t\t<div>\n\t\t\t<h3 className=\"text-xl font-medium mb-2\">\n\t\t\t\t<Trans>Tokens & Fingerprints</Trans>\n\t\t\t</h3>\n\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t<Trans>Tokens and fingerprints are used to authenticate WebSocket connections to the hub.</Trans>\n\t\t\t</p>\n\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed mt-1.5\">\n\t\t\t\t<Trans>\n\t\t\t\t\tTokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on\n\t\t\t\t\tfirst connection.\n\t\t\t\t</Trans>\n\t\t\t</p>\n\t\t</div>\n\t)\n})\n\nconst SectionUniversalToken = memo(() => {\n\tconst [token, setToken] = useState(\"\")\n\tconst [isLoading, setIsLoading] = useState(true)\n\tconst [checked, setChecked] = useState(false)\n\tconst [isPermanent, setIsPermanent] = useState(false)\n\n\tasync function updateToken(enable: number = -1, permanent: number = -1) {\n\t\t// enable: 0 for disable, 1 for enable, -1 (unset) for get current state\n\t\tconst data = await pb.send(`/api/beszel/universal-token`, {\n\t\t\tquery: {\n\t\t\t\ttoken,\n\t\t\t\tenable,\n\t\t\t\tpermanent,\n\t\t\t},\n\t\t})\n\t\tsetToken(data.token)\n\t\tsetChecked(data.active)\n\t\tsetIsPermanent(!!data.permanent)\n\t\tsetIsLoading(false)\n\t}\n\n\tuseEffect(() => {\n\t\tupdateToken()\n\t}, [])\n\n\treturn (\n\t\t<div>\n\t\t\t<h3 className=\"text-lg font-medium mb-2\">\n\t\t\t\t<Trans>Universal token</Trans>\n\t\t\t</h3>\n\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t<Trans>When enabled, this token allows agents to self-register without prior system creation.</Trans>\n\t\t\t</p>\n\t\t\t<div className=\"mt-3 border rounded-md px-4 py-3 max-w-full\">\n\t\t\t\t{!isLoading && (\n\t\t\t\t\t<div className=\"flex flex-col gap-3\">\n\t\t\t\t\t\t<div className=\"flex items-center gap-4 min-w-0\">\n\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\tchecked={checked}\n\t\t\t\t\t\t\t\tonCheckedChange={(checked) => {\n\t\t\t\t\t\t\t\t\t// Keep current permanence preference when enabling/disabling\n\t\t\t\t\t\t\t\t\tupdateToken(checked ? 1 : 0, isPermanent ? 1 : 0)\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<div className=\"min-w-0 flex-1 overflow-auto\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\"text-sm text-primary opacity-60 transition-opacity\",\n\t\t\t\t\t\t\t\t\t\tchecked ? \"opacity-100\" : \"select-none\"\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{token}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<ActionsButtonUniversalToken token={token} checked={checked} />\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{checked && (\n\t\t\t\t\t\t\t<div className=\"border-t pt-3\">\n\t\t\t\t\t\t\t\t<div className=\"text-sm font-medium\">\n\t\t\t\t\t\t\t\t\t<Trans>Persistence</Trans>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<Tabs\n\t\t\t\t\t\t\t\t\tvalue={isPermanent ? \"permanent\" : \"ephemeral\"}\n\t\t\t\t\t\t\t\t\tonValueChange={(value) => updateToken(1, value === \"permanent\" ? 1 : 0)}\n\t\t\t\t\t\t\t\t\tclassName=\"mt-2\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<TabsList>\n\t\t\t\t\t\t\t\t\t\t<TabsTrigger className=\"xs:min-w-40\" value=\"ephemeral\">\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Ephemeral</Trans>\n\t\t\t\t\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t\t\t\t\t\t<TabsTrigger className=\"xs:min-w-40\" value=\"permanent\">\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Permanent</Trans>\n\t\t\t\t\t\t\t\t\t\t</TabsTrigger>\n\t\t\t\t\t\t\t\t\t</TabsList>\n\t\t\t\t\t\t\t\t\t<TabsContent value=\"ephemeral\" className=\"mt-3\">\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Expires after one hour or on hub restart.</Trans>\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t\t\t\t\t<TabsContent value=\"permanent\" className=\"mt-3\">\n\t\t\t\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground leading-relaxed\">\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Saved in the database and does not expire until you disable it.</Trans>\n\t\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t\t</TabsContent>\n\t\t\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t)\n})\n\nconst ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; checked: boolean }) => {\n\tconst { t } = useLingui()\n\tconst publicKey = $publicKey.get()\n\tconst port = \"45876\"\n\n\tconst dropdownItems: DropdownItem[] = [\n\t\t{\n\t\t\ttext: t({ message: \"Copy docker compose\", context: \"Button to copy docker compose file content\" }),\n\t\t\tonClick: () => copyDockerCompose(port, publicKey, token),\n\t\t\ticons: [DockerIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t({ message: \"Copy docker run\", context: \"Button to copy docker run command\" }),\n\t\t\tonClick: () => copyDockerRun(port, publicKey, token),\n\t\t\ticons: [DockerIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t`Copy Linux command`,\n\t\t\tonClick: () => copyLinuxCommand(port, publicKey, token),\n\t\t\ticons: [TuxIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t({ message: \"Homebrew command\", context: \"Button to copy install command\" }),\n\t\t\tonClick: () => copyLinuxCommand(port, publicKey, token, true),\n\t\t\ticons: [TuxIcon, AppleIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t({ message: \"Windows command\", context: \"Button to copy install command\" }),\n\t\t\tonClick: () => copyWindowsCommand(port, publicKey, token),\n\t\t\ticons: [WindowsIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t({ message: \"FreeBSD command\", context: \"Button to copy install command\" }),\n\t\t\tonClick: () => copyLinuxCommand(port, publicKey, token),\n\t\t\ticons: [FreeBsdIcon],\n\t\t},\n\t\t{\n\t\t\ttext: t`Manual setup instructions`,\n\t\t\turl: \"https://beszel.dev/guide/agent-installation#binary\",\n\t\t\ticons: [ExternalLinkIcon],\n\t\t},\n\t]\n\treturn (\n\t\t<div className=\"flex items-center gap-2\">\n\t\t\t<DropdownMenu>\n\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\tdisabled={!checked}\n\t\t\t\t\t\tclassName={cn(\"transition-opacity\", !checked && \"opacity-50\")}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t\t<Trans>Open menu</Trans>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<MoreHorizontalIcon className=\"w-5\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t<InstallDropdown items={dropdownItems} />\n\t\t\t</DropdownMenu>\n\t\t</div>\n\t)\n})\n\nconst SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRecord[] }) => {\n\tconst { t } = useLingui()\n\tconst isReadOnly = isReadOnlyUser()\n\n\tconst headerCols = useMemo(\n\t\t() => [\n\t\t\t{\n\t\t\t\tlabel: t`System`,\n\t\t\t\tIcon: ServerIcon,\n\t\t\t\tw: \"11em\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: t`Token`,\n\t\t\t\tIcon: KeyIcon,\n\t\t\t\tw: \"20em\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: t`Fingerprint`,\n\t\t\t\tIcon: FingerprintIcon,\n\t\t\t\tw: \"20em\",\n\t\t\t},\n\t\t],\n\t\t[t]\n\t)\n\treturn (\n\t\t<div className=\"rounded-md border overflow-hidden w-full mt-4\">\n\t\t\t<Table>\n\t\t\t\t<TableHeader>\n\t\t\t\t\t<tr className=\"border-border/50\">\n\t\t\t\t\t\t{headerCols.map((col) => (\n\t\t\t\t\t\t\t<TableHead key={col.label} style={{ minWidth: col.w }}>\n\t\t\t\t\t\t\t\t<span className=\"flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t<col.Icon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t{col.label}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{!isReadOnly && (\n\t\t\t\t\t\t\t<TableHead className=\"w-0\">\n\t\t\t\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t\t\t\t<Trans>Actions</Trans>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</tr>\n\t\t\t\t</TableHeader>\n\t\t\t\t<TableBody className=\"whitespace-pre\">\n\t\t\t\t\t{fingerprints.map((fingerprint) => (\n\t\t\t\t\t\t<TableRow key={fingerprint.id}>\n\t\t\t\t\t\t\t<TableCell className=\"font-medium ps-5 py-2 max-w-60 truncate\">\n\t\t\t\t\t\t\t\t{fingerprint.expand.system.name}\n\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t<TableCell className=\"font-mono text-[0.95em] py-2\">{fingerprint.token}</TableCell>\n\t\t\t\t\t\t\t<TableCell className=\"font-mono text-[0.95em] py-2\">{fingerprint.fingerprint}</TableCell>\n\t\t\t\t\t\t\t{!isReadOnly && (\n\t\t\t\t\t\t\t\t<TableCell className=\"py-2 px-4 xl:px-2\">\n\t\t\t\t\t\t\t\t\t<ActionsButtonTable fingerprint={fingerprint} />\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t))}\n\t\t\t\t</TableBody>\n\t\t\t</Table>\n\t\t</div>\n\t)\n})\n\nasync function updateFingerprint(fingerprint: FingerprintRecord, rotateToken = false) {\n\ttry {\n\t\tawait pb.collection(\"fingerprints\").update(fingerprint.id, {\n\t\t\tfingerprint: \"\",\n\t\t\ttoken: rotateToken ? generateToken() : fingerprint.token,\n\t\t})\n\t} catch (error: unknown) {\n\t\ttoast({\n\t\t\ttitle: t`Error`,\n\t\t\tdescription: (error as Error).message,\n\t\t})\n\t}\n}\n\nconst ActionsButtonTable = memo(({ fingerprint }: { fingerprint: FingerprintRecord }) => {\n\tconst envVar = `HUB_URL=${getHubURL()}\\nTOKEN=${fingerprint.token}`\n\tconst copyEnv = () => copyToClipboard(envVar)\n\tconst copyYaml = () => copyToClipboard(envVar.replaceAll(\"=\", \": \"))\n\n\treturn (\n\t\t<DropdownMenu>\n\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t<Button variant=\"ghost\" size={\"icon\"} data-nolink>\n\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t<Trans>Open menu</Trans>\n\t\t\t\t\t</span>\n\t\t\t\t\t<MoreHorizontalIcon className=\"w-5\" />\n\t\t\t\t</Button>\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent align=\"end\">\n\t\t\t\t<DropdownMenuItem onClick={copyYaml}>\n\t\t\t\t\t<CopyIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t<Trans>Copy YAML</Trans>\n\t\t\t\t</DropdownMenuItem>\n\t\t\t\t<DropdownMenuItem onClick={copyEnv}>\n\t\t\t\t\t<CopyIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t<Trans context=\"Environment variables\">Copy env</Trans>\n\t\t\t\t</DropdownMenuItem>\n\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t<DropdownMenuItem onSelect={() => updateFingerprint(fingerprint, true)}>\n\t\t\t\t\t<RotateCwIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t<Trans>Rotate token</Trans>\n\t\t\t\t</DropdownMenuItem>\n\t\t\t\t{fingerprint.fingerprint && (\n\t\t\t\t\t<DropdownMenuItem onSelect={() => updateFingerprint(fingerprint)}>\n\t\t\t\t\t\t<Trash2Icon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t<Trans>Delete fingerprint</Trans>\n\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t)}\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t)\n})\n\nexport default SettingsFingerprintsPage\n"
  },
  {
    "path": "internal/site/src/components/routes/smart.tsx",
    "content": "import { useEffect } from \"react\"\nimport SmartTable from \"@/components/routes/system/smart-table\"\nimport { ActiveAlerts } from \"@/components/active-alerts\"\nimport { FooterRepoLink } from \"@/components/footer-repo-link\"\n\nexport default function Smart() {\n\tuseEffect(() => {\n\t\tdocument.title = `S.M.A.R.T. / Beszel`\n\t}, [])\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"grid gap-4\">\n\t\t\t\t<ActiveAlerts />\n\t\t\t\t<SmartTable />\n\t\t\t</div>\n\t\t\t<FooterRepoLink />\n\t\t</>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/system/cpu-sheet.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { MoreHorizontalIcon } from \"lucide-react\"\nimport { memo, useRef, useState } from \"react\"\nimport AreaChartDefault, { DataPoint } from \"@/components/charts/area-chart\"\nimport ChartTimeSelect from \"@/components/charts/chart-time-select\"\nimport { Button } from \"@/components/ui/button\"\nimport { Sheet, SheetContent, SheetTrigger } from \"@/components/ui/sheet\"\nimport { DialogTitle } from \"@/components/ui/dialog\"\nimport { compareSemVer, decimalString, parseSemVer, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData, SystemStatsRecord } from \"@/types\"\nimport { ChartCard } from \"../system\"\n\nconst minAgentVersion = parseSemVer(\"0.15.3\")\n\nexport default memo(function CpuCoresSheet({\n\tchartData,\n\tdataEmpty,\n\tgrid,\n\tmaxValues,\n}: {\n\tchartData: ChartData\n\tdataEmpty: boolean\n\tgrid: boolean\n\tmaxValues: boolean\n}) {\n\tconst [cpuCoresOpen, setCpuCoresOpen] = useState(false)\n\tconst hasOpened = useRef(false)\n\n\tconst supportsBreakdown = compareSemVer(chartData.agentVersion, minAgentVersion) >= 0\n\n\tif (!supportsBreakdown) {\n\t\treturn null\n\t}\n\n\tif (cpuCoresOpen && !hasOpened.current) {\n\t\thasOpened.current = true\n\t}\n\n\t// Latest stats snapshot\n\tconst latest = chartData.systemStats.at(-1)?.stats\n\tconst cpus = latest?.cpus ?? []\n\tconst numCores = cpus.length\n\tconst hasBreakdown = (latest?.cpub?.length ?? 0) > 0\n\n\tconst breakdownDataPoints = [\n\t\t{\n\t\t\tlabel: \"System\",\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[1],\n\t\t\tcolor: 3,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t\t{\n\t\t\tlabel: \"User\",\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[0],\n\t\t\tcolor: 1,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t\t{\n\t\t\tlabel: \"IOWait\",\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[2],\n\t\t\tcolor: 4,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t\t{\n\t\t\tlabel: \"Steal\",\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[3],\n\t\t\tcolor: 5,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t\t{\n\t\t\tlabel: \"Idle\",\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpub?.[4],\n\t\t\tcolor: 2,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t\t{\n\t\t\tlabel: t`Other`,\n\t\t\tdataKey: ({ stats }: SystemStatsRecord) => {\n\t\t\t\tconst total = stats?.cpub?.reduce((acc, curr) => acc + curr, 0) ?? 0\n\t\t\t\treturn total > 0 ? 100 - total : null\n\t\t\t},\n\t\t\tcolor: `hsl(80, 65%, 52%)`,\n\t\t\topacity: 0.35,\n\t\t\tstackId: \"a\"\n\t\t},\n\t] as DataPoint[]\n\n\n\treturn (\n\t\t<Sheet open={cpuCoresOpen} onOpenChange={setCpuCoresOpen}>\n\t\t\t<DialogTitle className=\"sr-only\">{t`CPU Usage`}</DialogTitle>\n\t\t\t<SheetTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\ttitle={t`View more`}\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tclassName=\"shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3\"\n\t\t\t\t>\n\t\t\t\t\t<MoreHorizontalIcon />\n\t\t\t\t</Button>\n\t\t\t</SheetTrigger>\n\t\t\t{hasOpened.current && (\n\t\t\t\t<SheetContent aria-describedby={undefined} className=\"overflow-auto w-200 !max-w-full p-4 sm:p-6\">\n\t\t\t\t\t<ChartTimeSelect className=\"w-[calc(100%-2em)] bg-card\" agentVersion={chartData.agentVersion} />\n\t\t\t\t\t{hasBreakdown && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tkey=\"cpu-breakdown\"\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`CPU Time Breakdown`}\n\t\t\t\t\t\t\tdescription={t`Percentage of time spent in each state`}\n\t\t\t\t\t\t\tlegend={true}\n\t\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\t\tlegend={true}\n\t\t\t\t\t\t\t\tdataPoints={breakdownDataPoints}\n\t\t\t\t\t\t\t\ttickFormatter={(val) => `${toFixedFloat(val, 2)}%`}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${decimalString(value)}%`}\n\t\t\t\t\t\t\t\treverseStackOrder={true}\n\t\t\t\t\t\t\t\titemSorter={() => 1}\n\t\t\t\t\t\t\t\tdomain={[0, 100]}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{numCores > 0 && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tkey=\"cpu-cores-all\"\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`CPU Cores`}\n\t\t\t\t\t\t\tlegend={numCores < 10}\n\t\t\t\t\t\t\tdescription={t`Per-core average utilization`}\n\t\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\thideYAxis={true}\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\t\tlegend={numCores < 10}\n\t\t\t\t\t\t\t\tdataPoints={Array.from({ length: numCores }).map((_, i) => ({\n\t\t\t\t\t\t\t\t\tlabel: `CPU ${i}`,\n\t\t\t\t\t\t\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i] ?? 1 / (stats?.cpus?.length ?? 1),\n\t\t\t\t\t\t\t\t\tcolor: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, var(--chart-saturation), var(--chart-lightness))`,\n\t\t\t\t\t\t\t\t\topacity: 0.35,\n\t\t\t\t\t\t\t\t\tstackId: \"a\"\n\t\t\t\t\t\t\t\t}))}\n\t\t\t\t\t\t\t\ttickFormatter={(val) => `${val}%`}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${value}%`}\n\t\t\t\t\t\t\t\treverseStackOrder={true}\n\t\t\t\t\t\t\t\titemSorter={() => 1}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{Array.from({ length: numCores }).map((_, i) => (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tkey={`cpu-core-${i}`}\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={`CPU ${i}`}\n\t\t\t\t\t\t\tdescription={t`Per-core average utilization`}\n\t\t\t\t\t\t\tlegend={false}\n\t\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\t\tlegend={false}\n\t\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tlabel: t`Usage`,\n\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.cpus?.[i],\n\t\t\t\t\t\t\t\t\t\tcolor: `hsl(${226 + (((i * 360) / Math.max(1, numCores)) % 360)}, 65%, 52%)`,\n\t\t\t\t\t\t\t\t\t\topacity: 0.35,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t\ttickFormatter={(val) => `${val}%`}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${value}%`}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t))}\n\t\t\t\t</SheetContent>\n\t\t\t)}\n\t\t</Sheet>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/routes/system/info-bar.tsx",
    "content": "import { plural } from \"@lingui/core/macro\"\nimport { useLingui } from \"@lingui/react/macro\"\nimport {\n\tAppleIcon,\n\tChevronRightSquareIcon,\n\tClockArrowUp,\n\tCpuIcon,\n\tGlobeIcon,\n\tLayoutGridIcon,\n\tMemoryStickIcon,\n\tMonitorIcon,\n\tRows,\n} from \"lucide-react\"\nimport { useMemo } from \"react\"\nimport ChartTimeSelect from \"@/components/charts/chart-time-select\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card } from \"@/components/ui/card\"\nimport { FreeBsdIcon, TuxIcon, WebSocketIcon, WindowsIcon } from \"@/components/ui/icons\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\"\nimport { ConnectionType, connectionTypeLabels, Os, SystemStatus } from \"@/lib/enums\"\nimport { cn, formatBytes, getHostDisplayValue, secondsToUptimeString, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData, SystemDetailsRecord, SystemRecord } from \"@/types\"\n\nexport default function InfoBar({\n\tsystem,\n\tchartData,\n\tgrid,\n\tsetGrid,\n\tdetails,\n}: {\n\tsystem: SystemRecord\n\tchartData: ChartData\n\tgrid: boolean\n\tsetGrid: (grid: boolean) => void\n\tdetails: SystemDetailsRecord | null\n}) {\n\tconst { t } = useLingui()\n\n\t// values for system info bar - use details with fallback to system.info\n\tconst systemInfo = useMemo(() => {\n\t\tif (!system.info) {\n\t\t\treturn []\n\t\t}\n\n\t\t// Use details if available, otherwise fall back to system.info\n\t\tconst hostname = details?.hostname ?? system.info.h\n\t\tconst kernel = details?.kernel ?? system.info.k\n\t\tconst cores = details?.cores ?? system.info.c\n\t\tconst threads = details?.threads ?? system.info.t ?? 0\n\t\tconst cpuModel = details?.cpu ?? system.info.m\n\t\tconst os = details?.os ?? system.info.os ?? Os.Linux\n\t\tconst osName = details?.os_name\n\t\tconst arch = details?.arch\n\t\tconst memory = details?.memory\n\n\t\tconst osInfo = {\n\t\t\t[Os.Linux]: {\n\t\t\t\tIcon: TuxIcon,\n\t\t\t\t// show kernel in tooltip if os name is available, otherwise show the kernel\n\t\t\t\tvalue: osName || kernel,\n\t\t\t\tlabel: osName ? kernel : undefined,\n\t\t\t},\n\t\t\t[Os.Darwin]: {\n\t\t\t\tIcon: AppleIcon,\n\t\t\t\tvalue: osName || `macOS ${kernel}`,\n\t\t\t},\n\t\t\t[Os.Windows]: {\n\t\t\t\tIcon: WindowsIcon,\n\t\t\t\tvalue: osName || kernel,\n\t\t\t\tlabel: osName ? kernel : undefined,\n\t\t\t},\n\t\t\t[Os.FreeBSD]: {\n\t\t\t\tIcon: FreeBsdIcon,\n\t\t\t\tvalue: osName || kernel,\n\t\t\t\tlabel: osName ? kernel : undefined,\n\t\t\t},\n\t\t}\n\n\t\tconst info = [\n\t\t\t{ value: getHostDisplayValue(system), Icon: GlobeIcon },\n\t\t\t{\n\t\t\t\tvalue: hostname,\n\t\t\t\tIcon: MonitorIcon,\n\t\t\t\tlabel: \"Hostname\",\n\t\t\t\t// hide if hostname is same as host or name\n\t\t\t\thide: hostname === system.host || hostname === system.name,\n\t\t\t},\n\t\t\t{ value: secondsToUptimeString(system.info.u), Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },\n\t\t\tosInfo[os],\n\t\t\t{\n\t\t\t\tvalue: cpuModel,\n\t\t\t\tIcon: CpuIcon,\n\t\t\t\thide: !cpuModel,\n\t\t\t\tlabel: `${plural(cores, { one: \"# core\", other: \"# cores\" })} / ${plural(threads, { one: \"# thread\", other: \"# threads\" })}${arch ? ` / ${arch}` : \"\"}`,\n\t\t\t},\n\t\t] as {\n\t\t\tvalue: string | number | undefined\n\t\t\tlabel?: string\n\t\t\tIcon: React.ElementType\n\t\t\thide?: boolean\n\t\t}[]\n\n\t\tif (memory) {\n\t\t\tconst memValue = formatBytes(memory, false, undefined, false)\n\t\t\tinfo.push({\n\t\t\t\tvalue: `${toFixedFloat(memValue.value, memValue.value >= 10 ? 1 : 2)} ${memValue.unit}`,\n\t\t\t\tIcon: MemoryStickIcon,\n\t\t\t\thide: !memory,\n\t\t\t\tlabel: t`Memory`,\n\t\t\t})\n\t\t}\n\n\t\treturn info\n\t}, [system, details, t])\n\n\tlet translatedStatus: string = system.status\n\tif (system.status === SystemStatus.Up) {\n\t\ttranslatedStatus = t({ message: \"Up\", comment: \"Context: System is up\" })\n\t} else if (system.status === SystemStatus.Down) {\n\t\ttranslatedStatus = t({ message: \"Down\", comment: \"Context: System is down\" })\n\t}\n\n\treturn (\n\t\t<Card>\n\t\t\t<div className=\"grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5\">\n\t\t\t\t<div>\n\t\t\t\t\t<h1 className=\"text-[1.6rem] font-semibold mb-1.5\">{system.name}</h1>\n\t\t\t\t\t<div className=\"flex flex-wrap items-center gap-3 gap-y-2 text-sm opacity-90\">\n\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t<div className=\"capitalize flex gap-2 items-center\">\n\t\t\t\t\t\t\t\t\t<span className={cn(\"relative flex h-3 w-3\")}>\n\t\t\t\t\t\t\t\t\t\t{system.status === SystemStatus.Up && (\n\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75\"\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ animationDuration: \"1.5s\" }}\n\t\t\t\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"relative inline-flex rounded-full h-3 w-3\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\"bg-green-500\": system.status === SystemStatus.Up,\n\t\t\t\t\t\t\t\t\t\t\t\t\"bg-red-500\": system.status === SystemStatus.Down,\n\t\t\t\t\t\t\t\t\t\t\t\t\"bg-primary/40\": system.status === SystemStatus.Paused,\n\t\t\t\t\t\t\t\t\t\t\t\t\"bg-yellow-500\": system.status === SystemStatus.Pending,\n\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{translatedStatus}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t{system.info.ct && (\n\t\t\t\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t\t\t\t<div className=\"flex gap-1 items-center\">\n\t\t\t\t\t\t\t\t\t\t{system.info.ct === ConnectionType.WebSocket ? (\n\t\t\t\t\t\t\t\t\t\t\t<WebSocketIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<ChevronRightSquareIcon className=\"size-4\" strokeWidth={2} />\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t{connectionTypeLabels[system.info.ct as ConnectionType]}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</TooltipContent>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Tooltip>\n\n\t\t\t\t\t\t{systemInfo.map(({ value, label, Icon, hide }) => {\n\t\t\t\t\t\t\tif (hide || !value) {\n\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst content = (\n\t\t\t\t\t\t\t\t<div className=\"flex gap-1.5 items-center\">\n\t\t\t\t\t\t\t\t\t<Icon className=\"h-4 w-4\" /> {value}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={value} className=\"contents\">\n\t\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-4 bg-primary/30\" />\n\t\t\t\t\t\t\t\t\t{label ? (\n\t\t\t\t\t\t\t\t\t\t<Tooltip delayDuration={100}>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>{content}</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t\t\t<TooltipContent>{label}</TooltipContent>\n\t\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\tcontent\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"xl:ms-auto flex items-center gap-2 max-sm:-mb-1\">\n\t\t\t\t\t<ChartTimeSelect className=\"w-full xl:w-40\" agentVersion={chartData.agentVersion} />\n\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\taria-label={t`Toggle grid`}\n\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\tclassName=\"hidden xl:flex p-0 text-primary\"\n\t\t\t\t\t\t\t\tonClick={() => setGrid(!grid)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{grid ? (\n\t\t\t\t\t\t\t\t\t<LayoutGridIcon className=\"h-[1.2rem] w-[1.2rem] opacity-75\" />\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<Rows className=\"h-[1.3rem] w-[1.3rem] opacity-75\" />\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t<TooltipContent>{t`Toggle grid`}</TooltipContent>\n\t\t\t\t\t</Tooltip>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/system/network-sheet.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { MoreHorizontalIcon } from \"lucide-react\"\nimport { memo, useRef, useState } from \"react\"\nimport AreaChartDefault from \"@/components/charts/area-chart\"\nimport ChartTimeSelect from \"@/components/charts/chart-time-select\"\nimport { useNetworkInterfaces } from \"@/components/charts/hooks\"\nimport { Button } from \"@/components/ui/button\"\nimport { Sheet, SheetContent, SheetTrigger } from \"@/components/ui/sheet\"\nimport { DialogTitle } from \"@/components/ui/dialog\"\nimport { $userSettings } from \"@/lib/stores\"\nimport { decimalString, formatBytes, toFixedFloat } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { ChartCard } from \"../system\"\n\nexport default memo(function NetworkSheet({\n\tchartData,\n\tdataEmpty,\n\tgrid,\n\tmaxValues,\n}: {\n\tchartData: ChartData\n\tdataEmpty: boolean\n\tgrid: boolean\n\tmaxValues: boolean\n}) {\n\tconst [netInterfacesOpen, setNetInterfacesOpen] = useState(false)\n\tconst userSettings = useStore($userSettings)\n\tconst netInterfaces = useNetworkInterfaces(chartData.systemStats.at(-1)?.stats?.ni ?? {})\n\tconst showNetLegend = netInterfaces.length > 0 && netInterfaces.length < 15\n\tconst hasOpened = useRef(false)\n\n\tif (netInterfacesOpen && !hasOpened.current) {\n\t\thasOpened.current = true\n\t}\n\n\tif (!netInterfaces.length) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<Sheet open={netInterfacesOpen} onOpenChange={setNetInterfacesOpen}>\n\t\t\t<DialogTitle className=\"sr-only\">{t`Network traffic of public interfaces`}</DialogTitle>\n\t\t\t<SheetTrigger asChild>\n\t\t\t\t<Button\n\t\t\t\t\ttitle={t`View more`}\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\tclassName=\"shrink-0 max-sm:absolute max-sm:top-3 max-sm:end-3\"\n\t\t\t\t>\n\t\t\t\t\t<MoreHorizontalIcon />\n\t\t\t\t</Button>\n\t\t\t</SheetTrigger>\n\t\t\t{hasOpened.current && (\n\t\t\t\t<SheetContent aria-describedby={undefined} className=\"overflow-auto w-200 !max-w-full p-4 sm:p-6\">\n\t\t\t\t\t<ChartTimeSelect className=\"w-[calc(100%-2em)] bg-card\" agentVersion={chartData.agentVersion} />\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Download`}\n\t\t\t\t\t\tdescription={t`Network traffic of public interfaces`}\n\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\titemSorter={(a, b) => b.value - a.value}\n\t\t\t\t\t\t\tdataPoints={netInterfaces.data(1)}\n\t\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Upload`}\n\t\t\t\t\t\tdescription={t`Network traffic of public interfaces`}\n\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\titemSorter={(a, b) => b.value - a.value}\n\t\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\t\tdataPoints={netInterfaces.data(0)}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Cumulative Download`}\n\t\t\t\t\t\tdescription={t`Total data received for each interface`}\n\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\t\tdataPoints={netInterfaces.data(3)}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, false, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Cumulative Upload`}\n\t\t\t\t\t\tdescription={t`Total data sent for each interface`}\n\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\tclassName=\"min-h-auto\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tlegend={showNetLegend}\n\t\t\t\t\t\t\tdataPoints={netInterfaces.data(2)}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, false, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, false, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\t\t\t\t</SheetContent>\n\t\t\t)}\n\t\t</Sheet>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/routes/system/smart-table.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport {\n\ttype ColumnDef,\n\ttype ColumnFiltersState,\n\ttype Column,\n\ttype Row,\n\ttype SortingState,\n\ttype Table as TableType,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetFilteredRowModel,\n\tgetSortedRowModel,\n\tuseReactTable,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer, type VirtualItem } from \"@tanstack/react-virtual\"\nimport {\n\tActivity,\n\tBox,\n\tClock,\n\tHardDrive,\n\tBinaryIcon,\n\tRotateCwIcon,\n\tLoaderCircleIcon,\n\tCheckCircle2Icon,\n\tXCircleIcon,\n\tArrowLeftRightIcon,\n\tMoreHorizontalIcon,\n\tRefreshCwIcon,\n\tServerIcon,\n\tTrash2Icon,\n\tXIcon,\n} from \"lucide-react\"\nimport { Card, CardHeader, CardTitle, CardDescription } from \"@/components/ui/card\"\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from \"@/components/ui/sheet\"\nimport { Input } from \"@/components/ui/input\"\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { pb } from \"@/lib/api\"\nimport type { SmartDeviceRecord, SmartAttribute } from \"@/types\"\nimport {\n\tformatBytes,\n\ttoFixedFloat,\n\tformatTemperature,\n\tcn,\n\tgetVisualStringWidth,\n\tsecondsToString,\n\thourWithSeconds,\n\tformatShortDate,\n} from \"@/lib/utils\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { $allSystemsById } from \"@/lib/stores\"\nimport { ThermometerIcon } from \"@/components/ui/icons\"\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { memo, useCallback, useMemo, useEffect, useRef, useState } from \"react\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\"\n\n// Column definition for S.M.A.R.T. attributes table\nexport const smartColumns: ColumnDef<SmartAttribute>[] = [\n\t{\n\t\taccessorKey: \"id\",\n\t\theader: \"ID\",\n\t},\n\t{\n\t\taccessorFn: (row) => row.n,\n\t\theader: \"Name\",\n\t},\n\t{\n\t\taccessorFn: (row) => row.rs || row.rv?.toString(),\n\t\theader: \"Value\",\n\t},\n\t{\n\t\taccessorKey: \"v\",\n\t\theader: \"Normalized\",\n\t},\n\t{\n\t\taccessorKey: \"w\",\n\t\theader: \"Worst\",\n\t},\n\t{\n\t\taccessorKey: \"t\",\n\t\theader: \"Threshold\",\n\t},\n\t{\n\t\t// accessorFn: (row) => row.wf,\n\t\taccessorKey: \"wf\",\n\t\theader: \"Failing\",\n\t},\n]\n\n// Function to format capacity display\nfunction formatCapacity(bytes: number): string {\n\tconst { value, unit } = formatBytes(bytes)\n\treturn `${toFixedFloat(value, value >= 10 ? 1 : 2)} ${unit}`\n}\n\nconst SMART_DEVICE_FIELDS = \"id,system,name,model,state,capacity,temp,type,hours,cycles,updated\"\n\nexport const createColumns = (\n\tlongestName: number,\n\tlongestModel: number,\n\tlongestDevice: number\n): ColumnDef<SmartDeviceRecord>[] => [\n\t{\n\t\tid: \"system\",\n\t\taccessorFn: (record) => record.system,\n\t\tsortingFn: (a, b) => {\n\t\t\tconst allSystems = $allSystemsById.get()\n\t\t\tconst systemNameA = allSystems[a.original.system]?.name ?? \"\"\n\t\t\tconst systemNameB = allSystems[b.original.system]?.name ?? \"\"\n\t\t\treturn systemNameA.localeCompare(systemNameB)\n\t\t},\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst allSystems = useStore($allSystemsById)\n\t\t\treturn (\n\t\t\t\t<div className=\"ms-1.5 max-w-40 block truncate\" style={{ width: `${longestName / 1.05}ch` }}>\n\t\t\t\t\t{allSystems[getValue() as string]?.name ?? \"\"}\n\t\t\t\t</div>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"name\",\n\t\tsortingFn: (a, b) => a.original.name.localeCompare(b.original.name),\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,\n\t\tcell: ({ getValue }) => (\n\t\t\t<div\n\t\t\t\tclassName=\"font-medium max-w-40 truncate ms-1\"\n\t\t\t\ttitle={getValue() as string}\n\t\t\t\tstyle={{ width: `${longestDevice / 1.05}ch` }}\n\t\t\t>\n\t\t\t\t{getValue() as string}\n\t\t\t</div>\n\t\t),\n\t},\n\t{\n\t\taccessorKey: \"model\",\n\t\tsortingFn: (a, b) => a.original.model.localeCompare(b.original.model),\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Model`} Icon={Box} />,\n\t\tcell: ({ getValue }) => (\n\t\t\t<div\n\t\t\t\tclassName=\"max-w-48 truncate ms-1\"\n\t\t\t\ttitle={getValue() as string}\n\t\t\t\tstyle={{ width: `${longestModel / 1.05}ch` }}\n\t\t\t>\n\t\t\t\t{getValue() as string}\n\t\t\t</div>\n\t\t),\n\t},\n\t{\n\t\taccessorKey: \"capacity\",\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Capacity`} Icon={BinaryIcon} />,\n\t\tcell: ({ getValue }) => <span className=\"ms-1\">{formatCapacity(getValue() as number)}</span>,\n\t},\n\t{\n\t\taccessorKey: \"state\",\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={Activity} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst status = getValue() as string\n\t\t\treturn (\n\t\t\t\t<Badge className=\"ms-1\" variant={status === \"PASSED\" ? \"success\" : status === \"FAILED\" ? \"danger\" : \"warning\"}>\n\t\t\t\t\t{status}\n\t\t\t\t</Badge>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"type\",\n\t\tsortingFn: (a, b) => a.original.type.localeCompare(b.original.type),\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Type`} Icon={ArrowLeftRightIcon} />,\n\t\tcell: ({ getValue }) => (\n\t\t\t<Badge variant=\"outline\" className=\"ms-1 uppercase\">\n\t\t\t\t{getValue() as string}\n\t\t\t</Badge>\n\t\t),\n\t},\n\t{\n\t\taccessorKey: \"hours\",\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => (\n\t\t\t<HeaderButton column={column} name={t({ message: \"Power On\", comment: \"Power On Time\" })} Icon={Clock} />\n\t\t),\n\t\tcell: ({ getValue }) => {\n\t\t\tconst hours = getValue() as number | undefined\n\t\t\tif (hours == null) {\n\t\t\t\treturn <div className=\"text-sm text-muted-foreground ms-1\">N/A</div>\n\t\t\t}\n\t\t\tconst seconds = hours * 3600\n\t\t\treturn (\n\t\t\t\t<div className=\"text-sm ms-1\">\n\t\t\t\t\t<div>{secondsToString(seconds, \"hour\")}</div>\n\t\t\t\t\t<div className=\"text-muted-foreground text-xs\">{secondsToString(seconds, \"day\")}</div>\n\t\t\t\t</div>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"cycles\",\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => (\n\t\t\t<HeaderButton column={column} name={t({ message: \"Cycles\", comment: \"Power Cycles\" })} Icon={RotateCwIcon} />\n\t\t),\n\t\tcell: ({ getValue }) => {\n\t\t\tconst cycles = getValue() as number | undefined\n\t\t\tif (cycles == null) {\n\t\t\t\treturn <div className=\"text-muted-foreground ms-1\">N/A</div>\n\t\t\t}\n\t\t\treturn <span className=\"ms-1\">{cycles.toLocaleString()}</span>\n\t\t},\n\t},\n\t{\n\t\taccessorKey: \"temp\",\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst temp = getValue() as number | null | undefined\n\t\t\tif (!temp) {\n\t\t\t\treturn <div className=\"text-muted-foreground ms-1\">N/A</div>\n\t\t\t}\n\t\t\tconst { value, unit } = formatTemperature(temp)\n\t\t\treturn <span className=\"ms-1\">{`${value} ${unit}`}</span>\n\t\t},\n\t},\n\t// {\n\t// \taccessorKey: \"serial\",\n\t// \tsortingFn: (a, b) => a.original.serial.localeCompare(b.original.serial),\n\t// \theader: ({ column }) => <HeaderButton column={column} name={t`Serial Number`} Icon={HashIcon} />,\n\t// \tcell: ({ getValue }) => <span className=\"ms-1.5\">{getValue() as string}</span>,\n\t// },\n\t// {\n\t// \taccessorKey: \"firmware\",\n\t// \tsortingFn: (a, b) => a.original.firmware.localeCompare(b.original.firmware),\n\t// \theader: ({ column }) => <HeaderButton column={column} name={t`Firmware`} Icon={CpuIcon} />,\n\t// \tcell: ({ getValue }) => <span className=\"ms-1.5\">{getValue() as string}</span>,\n\t// },\n\t{\n\t\tid: \"updated\",\n\t\tinvertSorting: true,\n\t\taccessorFn: (record) => record.updated,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={Clock} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst timestamp = getValue() as string\n\t\t\t// if today, use hourWithSeconds, otherwise use formatShortDate\n\t\t\tconst formatter =\n\t\t\t\tnew Date(timestamp).toDateString() === new Date().toDateString() ? hourWithSeconds : formatShortDate\n\t\t\treturn <span className=\"ms-1 tabular-nums\">{formatter(timestamp)}</span>\n\t\t},\n\t},\n]\n\nfunction HeaderButton({\n\tcolumn,\n\tname,\n\tIcon,\n}: {\n\tcolumn: Column<SmartDeviceRecord>\n\tname: string\n\tIcon: React.ElementType\n}) {\n\tconst isSorted = column.getIsSorted()\n\treturn (\n\t\t<Button\n\t\t\tclassName={cn(\n\t\t\t\t\"h-9 px-3 flex items-center gap-2 duration-50\",\n\t\t\t\tisSorted && \"bg-accent/70 light:bg-accent text-accent-foreground/90\"\n\t\t\t)}\n\t\t\tvariant=\"ghost\"\n\t\t\tonClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n\t\t>\n\t\t\t{Icon && <Icon className=\"size-4\" />}\n\t\t\t{name}\n\t\t</Button>\n\t)\n}\n\nexport default function DisksTable({ systemId }: { systemId?: string }) {\n\tconst [sorting, setSorting] = useState<SortingState>([{ id: systemId ? \"name\" : \"system\", desc: false }])\n\tconst [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n\tconst [rowSelection, setRowSelection] = useState({})\n\tconst [smartDevices, setSmartDevices] = useState<SmartDeviceRecord[] | undefined>(undefined)\n\tconst [activeDiskId, setActiveDiskId] = useState<string | null>(null)\n\tconst [sheetOpen, setSheetOpen] = useState(false)\n\tconst [rowActionState, setRowActionState] = useState<{ type: \"refresh\" | \"delete\"; id: string } | null>(null)\n\tconst [globalFilter, setGlobalFilter] = useState(\"\")\n\tconst allSystems = useStore($allSystemsById)\n\n\t// duplicate the devices to test with more rows\n\t// if (\n\t// \tsmartDevices?.length &&\n\t// \tsmartDevices.length < 50 &&\n\t// \ttypeof window !== \"undefined\" &&\n\t// \twindow.location.hostname === \"localhost\"\n\t// ) {\n\t// \tsetSmartDevices([...smartDevices, ...smartDevices, ...smartDevices])\n\t// }\n\n\t// Calculate the right width for the columns based on the longest strings among the displayed devices\n\tconst { longestName, longestModel, longestDevice } = useMemo(() => {\n\t\tconst result = { longestName: 0, longestModel: 0, longestDevice: 0 }\n\t\tif (!smartDevices || Object.keys(allSystems).length === 0) {\n\t\t\treturn result\n\t\t}\n\t\tconst seenSystems = new Set<string>()\n\t\tfor (const device of smartDevices) {\n\t\t\tif (!systemId && !seenSystems.has(device.system)) {\n\t\t\t\tseenSystems.add(device.system)\n\t\t\t\tconst name = allSystems[device.system]?.name ?? \"\"\n\t\t\t\tresult.longestName = Math.max(result.longestName, getVisualStringWidth(name))\n\t\t\t}\n\t\t\tresult.longestModel = Math.max(result.longestModel, getVisualStringWidth(device.model ?? \"\"))\n\t\t\tresult.longestDevice = Math.max(result.longestDevice, getVisualStringWidth(device.name ?? \"\"))\n\t\t}\n\t\treturn result\n\t}, [smartDevices, systemId, allSystems])\n\n\tconst openSheet = (disk: SmartDeviceRecord) => {\n\t\tsetActiveDiskId(disk.id)\n\t\tsetSheetOpen(true)\n\t}\n\n\t// Fetch smart devices\n\tuseEffect(() => {\n\t\tconst controller = new AbortController()\n\n\t\tpb.collection<SmartDeviceRecord>(\"smart_devices\")\n\t\t\t.getFullList({\n\t\t\t\tfilter: systemId ? pb.filter(\"system = {:system}\", { system: systemId }) : undefined,\n\t\t\t\tfields: SMART_DEVICE_FIELDS,\n\t\t\t\tsignal: controller.signal,\n\t\t\t})\n\t\t\t.then(setSmartDevices)\n\t\t\t.catch((err) => {\n\t\t\t\tif (!err.isAbort) {\n\t\t\t\t\tsetSmartDevices([])\n\t\t\t\t}\n\t\t\t})\n\n\t\treturn () => controller.abort()\n\t}, [systemId])\n\n\t// Subscribe to updates\n\tuseEffect(() => {\n\t\tlet unsubscribe: (() => void) | undefined\n\t\tconst pbOptions = systemId\n\t\t\t? { fields: SMART_DEVICE_FIELDS, filter: pb.filter(\"system = {:system}\", { system: systemId }) }\n\t\t\t: { fields: SMART_DEVICE_FIELDS }\n\n\t\t;(async () => {\n\t\t\ttry {\n\t\t\t\tunsubscribe = await pb.collection(\"smart_devices\").subscribe(\n\t\t\t\t\t\"*\",\n\t\t\t\t\t(event) => {\n\t\t\t\t\t\tconst record = event.record as SmartDeviceRecord\n\t\t\t\t\t\tsetSmartDevices((currentDevices) => {\n\t\t\t\t\t\t\tconst devices = currentDevices ?? []\n\t\t\t\t\t\t\tconst matchesSystemScope = !systemId || record.system === systemId\n\n\t\t\t\t\t\t\tif (event.action === \"delete\") {\n\t\t\t\t\t\t\t\treturn devices.filter((device) => device.id !== record.id)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (!matchesSystemScope) {\n\t\t\t\t\t\t\t\t// Record moved out of scope; ensure it disappears locally.\n\t\t\t\t\t\t\t\treturn devices.filter((device) => device.id !== record.id)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst existingIndex = devices.findIndex((device) => device.id === record.id)\n\t\t\t\t\t\t\tif (existingIndex === -1) {\n\t\t\t\t\t\t\t\treturn [record, ...devices]\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst next = [...devices]\n\t\t\t\t\t\t\tnext[existingIndex] = record\n\t\t\t\t\t\t\treturn next\n\t\t\t\t\t\t})\n\t\t\t\t\t},\n\t\t\t\t\tpbOptions\n\t\t\t\t)\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Failed to subscribe to SMART device updates:\", error)\n\t\t\t}\n\t\t})()\n\n\t\treturn () => {\n\t\t\tunsubscribe?.()\n\t\t}\n\t}, [systemId])\n\n\tconst handleRowRefresh = useCallback(async (disk: SmartDeviceRecord) => {\n\t\tif (!disk.system) return\n\t\tsetRowActionState({ type: \"refresh\", id: disk.id })\n\t\ttry {\n\t\t\tawait pb.send(\"/api/beszel/smart/refresh\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tquery: { system: disk.system },\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to refresh SMART device:\", error)\n\t\t} finally {\n\t\t\tsetRowActionState((state) => (state?.id === disk.id ? null : state))\n\t\t}\n\t}, [])\n\n\tconst handleDeleteDevice = useCallback(async (disk: SmartDeviceRecord) => {\n\t\tsetRowActionState({ type: \"delete\", id: disk.id })\n\t\ttry {\n\t\t\tawait pb.collection(\"smart_devices\").delete(disk.id)\n\t\t\t// setSmartDevices((current) => current?.filter((device) => device.id !== disk.id))\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to delete SMART device:\", error)\n\t\t} finally {\n\t\t\tsetRowActionState((state) => (state?.id === disk.id ? null : state))\n\t\t}\n\t}, [])\n\n\tconst actionColumn = useMemo<ColumnDef<SmartDeviceRecord>>(\n\t\t() => ({\n\t\t\tid: \"actions\",\n\t\t\tenableSorting: false,\n\t\t\theader: () => (\n\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t<Trans>Actions</Trans>\n\t\t\t\t</span>\n\t\t\t),\n\t\t\tcell: ({ row }) => {\n\t\t\t\tconst disk = row.original\n\t\t\t\tconst isRowRefreshing = rowActionState?.id === disk.id && rowActionState.type === \"refresh\"\n\t\t\t\tconst isRowDeleting = rowActionState?.id === disk.id && rowActionState.type === \"delete\"\n\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex justify-end\">\n\t\t\t\t\t\t<DropdownMenu>\n\t\t\t\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\tclassName=\"size-10\"\n\t\t\t\t\t\t\t\t\tonClick={(event) => event.stopPropagation()}\n\t\t\t\t\t\t\t\t\tonMouseDown={(event) => event.stopPropagation()}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t\t\t\t\t<Trans>Open menu</Trans>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<MoreHorizontalIcon className=\"w-5\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t\t\t\t<DropdownMenuContent align=\"end\" onClick={(event) => event.stopPropagation()}>\n\t\t\t\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\t\t\tevent.stopPropagation()\n\t\t\t\t\t\t\t\t\t\thandleRowRefresh(disk)\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tdisabled={isRowRefreshing || isRowDeleting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<RefreshCwIcon className={cn(\"me-2.5 size-4\", isRowRefreshing && \"animate-spin\")} />\n\t\t\t\t\t\t\t\t\t<Trans>Refresh</Trans>\n\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\t\t\tevent.stopPropagation()\n\t\t\t\t\t\t\t\t\t\thandleDeleteDevice(disk)\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tdisabled={isRowDeleting}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Trash2Icon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t\t<Trans>Delete</Trans>\n\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t</DropdownMenuContent>\n\t\t\t\t\t\t</DropdownMenu>\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t},\n\t\t}),\n\t\t[handleRowRefresh, handleDeleteDevice, rowActionState]\n\t)\n\n\t// Filter columns based on whether systemId is provided\n\tconst tableColumns = useMemo(() => {\n\t\tconst columns = createColumns(longestName, longestModel, longestDevice)\n\t\tconst baseColumns = systemId ? columns.filter((col) => col.id !== \"system\") : columns\n\t\treturn [...baseColumns, actionColumn]\n\t}, [systemId, actionColumn, longestName, longestModel, longestDevice])\n\n\tconst table = useReactTable({\n\t\tdata: smartDevices || ([] as SmartDeviceRecord[]),\n\t\tcolumns: tableColumns,\n\t\tonSortingChange: setSorting,\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tonRowSelectionChange: setRowSelection,\n\t\tstate: {\n\t\t\tsorting,\n\t\t\tcolumnFilters,\n\t\t\trowSelection,\n\t\t\tglobalFilter,\n\t\t},\n\t\tonGlobalFilterChange: setGlobalFilter,\n\t\tglobalFilterFn: (row, _columnId, filterValue) => {\n\t\t\tconst disk = row.original\n\t\t\tconst systemName = $allSystemsById.get()[disk.system]?.name ?? \"\"\n\t\t\tconst device = disk.name ?? \"\"\n\t\t\tconst model = disk.model ?? \"\"\n\t\t\tconst status = disk.state ?? \"\"\n\t\t\tconst type = disk.type ?? \"\"\n\t\t\tconst searchString = `${systemName} ${device} ${model} ${status} ${type}`.toLowerCase()\n\t\t\treturn (filterValue as string)\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(\" \")\n\t\t\t\t.every((term) => searchString.includes(term))\n\t\t},\n\t})\n\tconst rows = table.getRowModel().rows\n\n\t// Hide the table on system pages if there's no data, but always show on global page\n\tif (systemId && !smartDevices?.length && !columnFilters.length) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<Card className=\"p-6 @container w-full\">\n\t\t\t\t<CardHeader className=\"p-0 mb-4\">\n\t\t\t\t\t<div className=\"grid md:flex gap-5 w-full items-end\">\n\t\t\t\t\t\t<div className=\"px-2 sm:px-1\">\n\t\t\t\t\t\t\t<CardTitle className=\"mb-2\">S.M.A.R.T.</CardTitle>\n\t\t\t\t\t\t\t<CardDescription className=\"flex\">\n\t\t\t\t\t\t\t\t<Trans>Click on a device to view more information.</Trans>\n\t\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"relative ms-auto w-full max-w-full md:w-64\">\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\t\t\t\t\tvalue={globalFilter}\n\t\t\t\t\t\t\t\tonChange={(event) => setGlobalFilter(event.target.value)}\n\t\t\t\t\t\t\t\tclassName=\"px-4 w-full max-w-full md:w-64\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{globalFilter && (\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\taria-label={t`Clear`}\n\t\t\t\t\t\t\t\t\tclassName=\"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground\"\n\t\t\t\t\t\t\t\t\tonClick={() => setGlobalFilter(\"\")}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<XIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</CardHeader>\n\t\t\t\t<SmartDevicesTable\n\t\t\t\t\ttable={table}\n\t\t\t\t\trows={rows}\n\t\t\t\t\tcolLength={tableColumns.length}\n\t\t\t\t\tdata={smartDevices}\n\t\t\t\t\topenSheet={openSheet}\n\t\t\t\t/>\n\t\t\t</Card>\n\t\t\t<DiskSheet diskId={activeDiskId} open={sheetOpen} onOpenChange={setSheetOpen} />\n\t\t</div>\n\t)\n}\n\nconst SmartDevicesTable = memo(function SmartDevicesTable({\n\ttable,\n\trows,\n\tcolLength,\n\tdata,\n\topenSheet,\n}: {\n\ttable: TableType<SmartDeviceRecord>\n\trows: Row<SmartDeviceRecord>[]\n\tcolLength: number\n\tdata: SmartDeviceRecord[] | undefined\n\topenSheet: (disk: SmartDeviceRecord) => void\n}) {\n\tconst scrollRef = useRef<HTMLDivElement>(null)\n\n\tconst virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n\t\tcount: rows.length,\n\t\testimateSize: () => 65,\n\t\tgetScrollElement: () => scrollRef.current,\n\t\toverscan: 5,\n\t})\n\tconst virtualRows = virtualizer.getVirtualItems()\n\n\tconst paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)\n\tconst paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto rounded-md border\",\n\t\t\t\t(!rows.length || rows.length > 2) && \"min-h-50\"\n\t\t\t)}\n\t\t\tref={scrollRef}\n\t\t>\n\t\t\t<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>\n\t\t\t\t<table className=\"w-full text-sm text-nowrap\">\n\t\t\t\t\t<SmartTableHead table={table} />\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{rows.length ? (\n\t\t\t\t\t\t\tvirtualRows.map((virtualRow) => {\n\t\t\t\t\t\t\t\tconst row = rows[virtualRow.index]\n\t\t\t\t\t\t\t\treturn <SmartDeviceTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t<TableCell colSpan={colLength} className=\"h-24 text-center pointer-events-none\">\n\t\t\t\t\t\t\t\t\t{data ? t`No results.` : <LoaderCircleIcon className=\"animate-spin size-10 opacity-60 mx-auto\" />}\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</div>\n\t)\n})\n\nfunction SmartTableHead({ table }: { table: TableType<SmartDeviceRecord> }) {\n\treturn (\n\t\t<TableHeader className=\"sticky top-0 z-50 w-full border-b-2\">\n\t\t\t<div className=\"absolute -top-2 left-0 w-full h-4 bg-table-header z-50\"></div>\n\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t<TableRow key={headerGroup.id}>\n\t\t\t\t\t{headerGroup.headers.map((header) => (\n\t\t\t\t\t\t<TableHead key={header.id} className=\"px-2\">\n\t\t\t\t\t\t\t{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t))}\n\t\t\t\t</TableRow>\n\t\t\t))}\n\t\t</TableHeader>\n\t)\n}\n\nconst SmartDeviceTableRow = memo(function SmartDeviceTableRow({\n\trow,\n\tvirtualRow,\n\topenSheet,\n}: {\n\trow: Row<SmartDeviceRecord>\n\tvirtualRow: VirtualItem\n\topenSheet: (disk: SmartDeviceRecord) => void\n}) {\n\treturn (\n\t\t<TableRow\n\t\t\tdata-state={row.getIsSelected() && \"selected\"}\n\t\t\tclassName=\"cursor-pointer\"\n\t\t\tonClick={() => openSheet(row.original)}\n\t\t>\n\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t<TableCell\n\t\t\t\t\tkey={cell.id}\n\t\t\t\t\tclassName=\"md:ps-5 py-0\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: virtualRow.size,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t</TableCell>\n\t\t\t))}\n\t\t</TableRow>\n\t)\n})\n\nfunction DiskSheet({\n\tdiskId,\n\topen,\n\tonOpenChange,\n}: {\n\tdiskId: string | null\n\topen: boolean\n\tonOpenChange: (open: boolean) => void\n}) {\n\tconst [disk, setDisk] = useState<SmartDeviceRecord | null>(null)\n\tconst [isLoading, setIsLoading] = useState(false)\n\n\t// Fetch full device record (including attributes) when sheet opens\n\tuseEffect(() => {\n\t\tif (!diskId) {\n\t\t\tsetDisk(null)\n\t\t\treturn\n\t\t}\n\t\t// Only fetch when opening, not when closing (keeps data visible during close animation)\n\t\tif (!open) return\n\t\tsetIsLoading(true)\n\t\tpb.collection<SmartDeviceRecord>(\"smart_devices\")\n\t\t\t.getOne(diskId)\n\t\t\t.then(setDisk)\n\t\t\t.catch(() => setDisk(null))\n\t\t\t.finally(() => setIsLoading(false))\n\t}, [open, diskId])\n\n\tconst smartAttributes = disk?.attributes || []\n\n\t// Find all attributes where when failed is not empty\n\tconst failedAttributes = smartAttributes.filter((attr) => attr.wf && attr.wf.trim() !== \"\")\n\n\t// Filter columns to only show those that have values in at least one row\n\tconst visibleColumns = useMemo(() => {\n\t\treturn smartColumns.filter((column) => {\n\t\t\tconst accessorKey = \"accessorKey\" in column ? (column.accessorKey as keyof SmartAttribute | undefined) : undefined\n\t\t\tif (!accessorKey) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Check if any row has a non-empty value for this column\n\t\t\treturn smartAttributes.some((attr) => {\n\t\t\t\treturn attr[accessorKey] !== undefined\n\t\t\t})\n\t\t})\n\t}, [smartAttributes])\n\n\tconst table = useReactTable({\n\t\tdata: smartAttributes,\n\t\tcolumns: visibleColumns,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t})\n\n\tconst unknown = \"Unknown\"\n\tconst deviceName = disk?.name || unknown\n\tconst model = disk?.model || unknown\n\tconst capacity = disk?.capacity ? formatCapacity(disk.capacity) : unknown\n\tconst serialNumber = disk?.serial\n\tconst firmwareVersion = disk?.firmware\n\tconst status = disk?.state || unknown\n\n\treturn (\n\t\t<Sheet open={open} onOpenChange={onOpenChange}>\n\t\t\t<SheetContent className=\"w-full sm:max-w-220 gap-0\">\n\t\t\t\t<SheetHeader className=\"mb-0 border-b\">\n\t\t\t\t\t<SheetTitle>\n\t\t\t\t\t\t<Trans>S.M.A.R.T. Details</Trans> - {deviceName}\n\t\t\t\t\t</SheetTitle>\n\t\t\t\t\t<SheetDescription className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n\t\t\t\t\t\t{model}\n\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t{capacity}\n\t\t\t\t\t\t{serialNumber && (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t<span>{serialNumber}</span>\n\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t\t\t\t\t<Trans>Serial Number</Trans>\n\t\t\t\t\t\t\t\t\t</TooltipContent>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{firmwareVersion && (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-2.5 bg-muted-foreground opacity-70\" />\n\t\t\t\t\t\t\t\t<Tooltip>\n\t\t\t\t\t\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t\t\t\t\t\t<span>{firmwareVersion}</span>\n\t\t\t\t\t\t\t\t\t</TooltipTrigger>\n\t\t\t\t\t\t\t\t\t<TooltipContent>\n\t\t\t\t\t\t\t\t\t\t<Trans>Firmware</Trans>\n\t\t\t\t\t\t\t\t\t</TooltipContent>\n\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</SheetDescription>\n\t\t\t\t</SheetHeader>\n\t\t\t\t<div className=\"flex-1 overflow-hidden p-4 flex flex-col gap-4\">\n\t\t\t\t\t{isLoading ? (\n\t\t\t\t\t\t<div className=\"flex justify-center py-8\">\n\t\t\t\t\t\t\t<LoaderCircleIcon className=\"animate-spin size-10 opacity-60\" />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<Alert className=\"pb-3 shrink-0\">\n\t\t\t\t\t\t\t\t{status === \"PASSED\" ? <CheckCircle2Icon className=\"size-4\" /> : <XCircleIcon className=\"size-4\" />}\n\t\t\t\t\t\t\t\t<AlertTitle>\n\t\t\t\t\t\t\t\t\t<Trans>S.M.A.R.T. Self-Test</Trans>: {status}\n\t\t\t\t\t\t\t\t</AlertTitle>\n\t\t\t\t\t\t\t\t{failedAttributes.length > 0 && (\n\t\t\t\t\t\t\t\t\t<AlertDescription>\n\t\t\t\t\t\t\t\t\t\t<Trans>Failed Attributes:</Trans> {failedAttributes.map((attr) => attr.n).join(\", \")}\n\t\t\t\t\t\t\t\t\t</AlertDescription>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Alert>\n\t\t\t\t\t\t\t{smartAttributes.length > 0 ? (\n\t\t\t\t\t\t\t\t<div className=\"rounded-md border min-h-0 flex flex-col\">\n\t\t\t\t\t\t\t\t\t<Table>\n\t\t\t\t\t\t\t\t\t\t<TableHeader className=\"sticky top-0 z-10\">\n\t\t\t\t\t\t\t\t\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<TableRow key={headerGroup.id}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{headerGroup.headers.map((header) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TableHead key={header.id}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{header.isPlaceholder\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? null\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t</TableHeader>\n\t\t\t\t\t\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t\t\t\t\t\t{table.getRowModel().rows.map((row) => {\n\t\t\t\t\t\t\t\t\t\t\t\t// Check if the attribute is failed\n\t\t\t\t\t\t\t\t\t\t\t\tconst isFailedAttribute = row.original.wf && row.original.wf.trim() !== \"\"\n\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<TableRow key={row.id} className={isFailedAttribute ? \"text-red-600 dark:text-red-400\" : \"\"}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TableCell key={cell.id}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t</TableBody>\n\t\t\t\t\t\t\t\t\t</Table>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<div className=\"text-center py-8 text-muted-foreground\">\n\t\t\t\t\t\t\t\t\t<Trans>No S.M.A.R.T. attributes available for this device.</Trans>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</SheetContent>\n\t\t</Sheet>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/routes/system.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport { timeTicks } from \"d3-time\"\nimport { XIcon } from \"lucide-react\"\nimport { subscribeKeys } from \"nanostores\"\nimport React, { type JSX, lazy, memo, useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport AreaChartDefault, { type DataPoint } from \"@/components/charts/area-chart\"\nimport ContainerChart from \"@/components/charts/container-chart\"\nimport DiskChart from \"@/components/charts/disk-chart\"\nimport GpuPowerChart from \"@/components/charts/gpu-power-chart\"\nimport { useContainerChartConfigs } from \"@/components/charts/hooks\"\nimport LoadAverageChart from \"@/components/charts/load-average-chart\"\nimport MemChart from \"@/components/charts/mem-chart\"\nimport SwapChart from \"@/components/charts/swap-chart\"\nimport TemperatureChart from \"@/components/charts/temperature-chart\"\nimport { getPbTimestamp, pb } from \"@/lib/api\"\nimport { ChartType, SystemStatus, Unit } from \"@/lib/enums\"\nimport { batteryStateTranslations } from \"@/lib/i18n\"\nimport {\n\t$allSystemsById,\n\t$allSystemsByName,\n\t$chartTime,\n\t$containerFilter,\n\t$direction,\n\t$maxValues,\n\t$systems,\n\t$temperatureFilter,\n\t$userSettings,\n} from \"@/lib/stores\"\nimport { useIntersectionObserver } from \"@/lib/use-intersection-observer\"\nimport {\n\tchartTimeData,\n\tcn,\n\tcompareSemVer,\n\tdecimalString,\n\tformatBytes,\n\tlisten,\n\tparseSemVer,\n\ttoFixedFloat,\n\tuseBrowserStorage,\n} from \"@/lib/utils\"\nimport type {\n\tChartData,\n\tChartTimes,\n\tContainerStatsRecord,\n\tGPUData,\n\tSystemDetailsRecord,\n\tSystemInfo,\n\tSystemRecord,\n\tSystemStats,\n\tSystemStatsRecord,\n} from \"@/types\"\nimport { $router, navigate } from \"../router\"\nimport Spinner from \"../spinner\"\nimport { Button } from \"../ui/button\"\nimport { Card, CardDescription, CardHeader, CardTitle } from \"../ui/card\"\nimport { ChartAverage, ChartMax } from \"../ui/icons\"\nimport { Input } from \"../ui/input\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"../ui/select\"\nimport NetworkSheet from \"./system/network-sheet\"\nimport CpuCoresSheet from \"./system/cpu-sheet\"\nimport LineChartDefault from \"../charts/line-chart\"\nimport { pinnedAxisDomain } from \"../ui/chart\"\nimport InfoBar from \"./system/info-bar\"\n\ntype ChartTimeData = {\n\ttime: number\n\tdata: {\n\t\tticks: number[]\n\t\tdomain: number[]\n\t}\n\tchartTime: ChartTimes\n}\n\nconst cache = new Map<string, ChartTimeData | SystemStatsRecord[] | ContainerStatsRecord[]>()\n\n// create ticks and domain for charts\nfunction getTimeData(chartTime: ChartTimes, lastCreated: number) {\n\tconst cached = cache.get(\"td\") as ChartTimeData | undefined\n\tif (cached && cached.chartTime === chartTime) {\n\t\tif (!lastCreated || cached.time >= lastCreated) {\n\t\t\treturn cached.data\n\t\t}\n\t}\n\n\t// const buffer = chartTime === \"1m\" ? 400 : 20_000\n\tconst now = new Date(Date.now())\n\tconst startTime = chartTimeData[chartTime].getOffset(now)\n\tconst ticks = timeTicks(startTime, now, chartTimeData[chartTime].ticks ?? 12).map((date) => date.getTime())\n\tconst data = {\n\t\tticks,\n\t\tdomain: [chartTimeData[chartTime].getOffset(now).getTime(), now.getTime()],\n\t}\n\tcache.set(\"td\", { time: now.getTime(), data, chartTime })\n\treturn data\n}\n\n// add empty values between records to make gaps if interval is too large\nfunction addEmptyValues<T extends { created: string | number | null }>(\n\tprevRecords: T[],\n\tnewRecords: T[],\n\texpectedInterval: number\n): T[] {\n\tconst modifiedRecords: T[] = []\n\tlet prevTime = (prevRecords.at(-1)?.created ?? 0) as number\n\tfor (let i = 0; i < newRecords.length; i++) {\n\t\tconst record = newRecords[i]\n\t\tif (record.created !== null) {\n\t\t\trecord.created = new Date(record.created).getTime()\n\t\t}\n\t\tif (prevTime && record.created !== null) {\n\t\t\tconst interval = record.created - prevTime\n\t\t\t// if interval is too large, add a null record\n\t\t\tif (interval > expectedInterval / 2 + expectedInterval) {\n\t\t\t\tmodifiedRecords.push({ created: null, ...(\"stats\" in record ? { stats: null } : {}) } as T)\n\t\t\t}\n\t\t}\n\t\tif (record.created !== null) {\n\t\t\tprevTime = record.created\n\t\t}\n\t\tmodifiedRecords.push(record)\n\t}\n\treturn modifiedRecords\n}\n\nasync function getStats<T extends SystemStatsRecord | ContainerStatsRecord>(\n\tcollection: string,\n\tsystem: SystemRecord,\n\tchartTime: ChartTimes\n): Promise<T[]> {\n\tconst cachedStats = cache.get(`${system.id}_${chartTime}_${collection}`) as T[] | undefined\n\tconst lastCached = cachedStats?.at(-1)?.created as number\n\treturn await pb.collection<T>(collection).getFullList({\n\t\tfilter: pb.filter(\"system={:id} && created > {:created} && type={:type}\", {\n\t\t\tid: system.id,\n\t\t\tcreated: getPbTimestamp(chartTime, lastCached ? new Date(lastCached + 1000) : undefined),\n\t\t\ttype: chartTimeData[chartTime].type,\n\t\t}),\n\t\tfields: \"created,stats\",\n\t\tsort: \"created\",\n\t})\n}\n\nfunction dockerOrPodman(str: string, isPodman: boolean): string {\n\tif (isPodman) {\n\t\treturn str.replace(\"docker\", \"podman\").replace(\"Docker\", \"Podman\")\n\t}\n\treturn str\n}\n\nexport default memo(function SystemDetail({ id }: { id: string }) {\n\tconst direction = useStore($direction)\n\tconst { t } = useLingui()\n\tconst systems = useStore($systems)\n\tconst chartTime = useStore($chartTime)\n\tconst maxValues = useStore($maxValues)\n\tconst [grid, setGrid] = useBrowserStorage(\"grid\", true)\n\tconst [system, setSystem] = useState({} as SystemRecord)\n\tconst [systemStats, setSystemStats] = useState([] as SystemStatsRecord[])\n\tconst [containerData, setContainerData] = useState([] as ChartData[\"containerData\"])\n\tconst temperatureChartRef = useRef<HTMLDivElement>(null)\n\tconst persistChartTime = useRef(false)\n\tconst [bottomSpacing, setBottomSpacing] = useState(0)\n\tconst [chartLoading, setChartLoading] = useState(true)\n\tconst isLongerChart = ![\"1m\", \"1h\"].includes(chartTime) // true if chart time is not 1m or 1h\n\tconst userSettings = $userSettings.get()\n\tconst chartWrapRef = useRef<HTMLDivElement>(null)\n\tconst [details, setDetails] = useState<SystemDetailsRecord>({} as SystemDetailsRecord)\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (!persistChartTime.current) {\n\t\t\t\t$chartTime.set($userSettings.get().chartTime)\n\t\t\t}\n\t\t\tpersistChartTime.current = false\n\t\t\tsetSystemStats([])\n\t\t\tsetContainerData([])\n\t\t\tsetDetails({} as SystemDetailsRecord)\n\t\t\t$containerFilter.set(\"\")\n\t\t}\n\t}, [id])\n\n\t// find matching system and update when it changes\n\tuseEffect(() => {\n\t\tif (!systems.length) {\n\t\t\treturn\n\t\t}\n\t\t// allow old system-name slug to work\n\t\tconst store = $allSystemsById.get()[id] ? $allSystemsById : $allSystemsByName\n\t\treturn subscribeKeys(store, [id], (newSystems) => {\n\t\t\tconst sys = newSystems[id]\n\t\t\tif (sys) {\n\t\t\t\tsetSystem(sys)\n\t\t\t\tdocument.title = `${sys?.name} / Beszel`\n\t\t\t}\n\t\t})\n\t}, [id, systems.length])\n\n\t// hide 1m chart time if system agent version is less than 0.13.0\n\tuseEffect(() => {\n\t\tif (parseSemVer(system?.info?.v) < parseSemVer(\"0.13.0\")) {\n\t\t\t$chartTime.set(\"1h\")\n\t\t}\n\t}, [system?.info?.v])\n\n\t// fetch system details\n\tuseEffect(() => {\n\t\t// if system.info.m exists, agent is old version without system details\n\t\tif (!system.id || system.info?.m) {\n\t\t\treturn\n\t\t}\n\t\tpb.collection<SystemDetailsRecord>(\"system_details\")\n\t\t\t.getOne(system.id, {\n\t\t\t\tfields: \"hostname,kernel,cores,threads,cpu,os,os_name,arch,memory,podman\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Cache-Control\": \"public, max-age=60\",\n\t\t\t\t},\n\t\t\t})\n\t\t\t.then(setDetails)\n\t}, [system.id])\n\n\t// subscribe to realtime metrics if chart time is 1m\n\tuseEffect(() => {\n\t\tlet unsub = () => {}\n\t\tif (!system.id || chartTime !== \"1m\") {\n\t\t\treturn\n\t\t}\n\t\tif (system.status !== SystemStatus.Up || parseSemVer(system?.info?.v).minor < 13) {\n\t\t\t$chartTime.set(\"1h\")\n\t\t\treturn\n\t\t}\n\t\tpb.realtime\n\t\t\t.subscribe(\n\t\t\t\t`rt_metrics`,\n\t\t\t\t(data: { container: ContainerStatsRecord[]; info: SystemInfo; stats: SystemStats }) => {\n\t\t\t\t\tif (data.container?.length > 0) {\n\t\t\t\t\t\tconst newContainerData = makeContainerData([\n\t\t\t\t\t\t\t{ created: Date.now(), stats: data.container } as unknown as ContainerStatsRecord,\n\t\t\t\t\t\t])\n\t\t\t\t\t\tsetContainerData((prevData) => addEmptyValues(prevData, prevData.slice(-59).concat(newContainerData), 1000))\n\t\t\t\t\t}\n\t\t\t\t\tsetSystemStats((prevStats) =>\n\t\t\t\t\t\taddEmptyValues(\n\t\t\t\t\t\t\tprevStats,\n\t\t\t\t\t\t\tprevStats.slice(-59).concat({ created: Date.now(), stats: data.stats } as SystemStatsRecord),\n\t\t\t\t\t\t\t1000\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t\t{ query: { system: system.id } }\n\t\t\t)\n\t\t\t.then((us) => {\n\t\t\t\tunsub = us\n\t\t\t})\n\t\treturn () => {\n\t\t\tunsub?.()\n\t\t}\n\t}, [chartTime, system.id])\n\n\tconst chartData: ChartData = useMemo(() => {\n\t\tconst lastCreated = Math.max(\n\t\t\t(systemStats.at(-1)?.created as number) ?? 0,\n\t\t\t(containerData.at(-1)?.created as number) ?? 0\n\t\t)\n\t\treturn {\n\t\t\tsystemStats,\n\t\t\tcontainerData,\n\t\t\tchartTime,\n\t\t\torientation: direction === \"rtl\" ? \"right\" : \"left\",\n\t\t\t...getTimeData(chartTime, lastCreated),\n\t\t\tagentVersion: parseSemVer(system?.info?.v),\n\t\t}\n\t}, [systemStats, containerData, direction])\n\n\t// Share chart config computation for all container charts\n\tconst containerChartConfigs = useContainerChartConfigs(containerData)\n\n\t// make container stats for charts\n\tconst makeContainerData = useCallback((containers: ContainerStatsRecord[]) => {\n\t\tconst containerData = [] as ChartData[\"containerData\"]\n\t\tfor (let { created, stats } of containers) {\n\t\t\tif (!created) {\n\t\t\t\t// @ts-expect-error add null value for gaps\n\t\t\t\tcontainerData.push({ created: null })\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcreated = new Date(created).getTime()\n\t\t\t// @ts-expect-error not dealing with this rn\n\t\t\tconst containerStats: ChartData[\"containerData\"][0] = { created }\n\t\t\tfor (const container of stats) {\n\t\t\t\tcontainerStats[container.n] = container\n\t\t\t}\n\t\t\tcontainerData.push(containerStats)\n\t\t}\n\t\treturn containerData\n\t}, [])\n\n\t// get stats\n\tuseEffect(() => {\n\t\tif (!system.id || !chartTime || chartTime === \"1m\") {\n\t\t\treturn\n\t\t}\n\t\t// loading: true\n\t\tsetChartLoading(true)\n\t\tPromise.allSettled([\n\t\t\tgetStats<SystemStatsRecord>(\"system_stats\", system, chartTime),\n\t\t\tgetStats<ContainerStatsRecord>(\"container_stats\", system, chartTime),\n\t\t]).then(([systemStats, containerStats]) => {\n\t\t\t// loading: false\n\t\t\tsetChartLoading(false)\n\n\t\t\tconst { expectedInterval } = chartTimeData[chartTime]\n\t\t\t// make new system stats\n\t\t\tconst ss_cache_key = `${system.id}_${chartTime}_system_stats`\n\t\t\tlet systemData = (cache.get(ss_cache_key) || []) as SystemStatsRecord[]\n\t\t\tif (systemStats.status === \"fulfilled\" && systemStats.value.length) {\n\t\t\t\tsystemData = systemData.concat(addEmptyValues(systemData, systemStats.value, expectedInterval))\n\t\t\t\tif (systemData.length > 120) {\n\t\t\t\t\tsystemData = systemData.slice(-100)\n\t\t\t\t}\n\t\t\t\tcache.set(ss_cache_key, systemData)\n\t\t\t}\n\t\t\tsetSystemStats(systemData)\n\t\t\t// make new container stats\n\t\t\tconst cs_cache_key = `${system.id}_${chartTime}_container_stats`\n\t\t\tlet containerData = (cache.get(cs_cache_key) || []) as ContainerStatsRecord[]\n\t\t\tif (containerStats.status === \"fulfilled\" && containerStats.value.length) {\n\t\t\t\tcontainerData = containerData.concat(addEmptyValues(containerData, containerStats.value, expectedInterval))\n\t\t\t\tif (containerData.length > 120) {\n\t\t\t\t\tcontainerData = containerData.slice(-100)\n\t\t\t\t}\n\t\t\t\tcache.set(cs_cache_key, containerData)\n\t\t\t}\n\t\t\tsetContainerData(makeContainerData(containerData))\n\t\t})\n\t}, [system, chartTime])\n\n\t/** Space for tooltip if more than 10 sensors and no containers table */\n\tuseEffect(() => {\n\t\tconst sensors = Object.keys(systemStats.at(-1)?.stats.t ?? {})\n\t\tif (!temperatureChartRef.current || sensors.length < 10 || containerData.length > 0) {\n\t\t\tsetBottomSpacing(0)\n\t\t\treturn\n\t\t}\n\t\tconst tooltipHeight = (sensors.length - 10) * 17.8 - 40\n\t\tconst wrapperEl = chartWrapRef.current as HTMLDivElement\n\t\tconst wrapperRect = wrapperEl.getBoundingClientRect()\n\t\tconst chartRect = temperatureChartRef.current.getBoundingClientRect()\n\t\tconst distanceToBottom = wrapperRect.bottom - chartRect.bottom\n\t\tsetBottomSpacing(tooltipHeight - distanceToBottom)\n\t}, [])\n\n\t// keyboard navigation between systems\n\tuseEffect(() => {\n\t\tif (!systems.length) {\n\t\t\treturn\n\t\t}\n\t\tconst handleKeyUp = (e: KeyboardEvent) => {\n\t\t\tif (\n\t\t\t\te.target instanceof HTMLInputElement ||\n\t\t\t\te.target instanceof HTMLTextAreaElement ||\n\t\t\t\te.shiftKey ||\n\t\t\t\te.ctrlKey ||\n\t\t\t\te.metaKey ||\n\t\t\t\te.altKey\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst currentIndex = systems.findIndex((s) => s.id === id)\n\t\t\tif (currentIndex === -1 || systems.length <= 1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tswitch (e.key) {\n\t\t\t\tcase \"ArrowLeft\":\n\t\t\t\tcase \"h\": {\n\t\t\t\t\tconst prevIndex = (currentIndex - 1 + systems.length) % systems.length\n\t\t\t\t\tpersistChartTime.current = true\n\t\t\t\t\treturn navigate(getPagePath($router, \"system\", { id: systems[prevIndex].id }))\n\t\t\t\t}\n\t\t\t\tcase \"ArrowRight\":\n\t\t\t\tcase \"l\": {\n\t\t\t\t\tconst nextIndex = (currentIndex + 1) % systems.length\n\t\t\t\t\tpersistChartTime.current = true\n\t\t\t\t\treturn navigate(getPagePath($router, \"system\", { id: systems[nextIndex].id }))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn listen(document, \"keyup\", handleKeyUp)\n\t}, [id, systems])\n\n\tif (!system.id) {\n\t\treturn null\n\t}\n\n\t// select field for switching between avg and max values\n\tconst maxValSelect = isLongerChart ? <SelectAvgMax max={maxValues} /> : null\n\tconst showMax = maxValues && isLongerChart\n\n\tconst containerFilterBar = containerData.length ? <FilterBar /> : null\n\n\tconst dataEmpty = !chartLoading && chartData.systemStats.length === 0\n\tconst lastGpus = systemStats.at(-1)?.stats?.g\n\n\tlet hasGpuData = false\n\tlet hasGpuEnginesData = false\n\tlet hasGpuPowerData = false\n\n\tif (lastGpus) {\n\t\t// check if there are any GPUs at all\n\t\thasGpuData = Object.keys(lastGpus).length > 0\n\t\t// check if there are any GPUs with engines or power data\n\t\tfor (let i = 0; i < systemStats.length && (!hasGpuEnginesData || !hasGpuPowerData); i++) {\n\t\t\tconst gpus = systemStats[i].stats?.g\n\t\t\tif (!gpus) continue\n\t\t\tfor (const id in gpus) {\n\t\t\t\tif (!hasGpuEnginesData && gpus[id].e !== undefined) {\n\t\t\t\t\thasGpuEnginesData = true\n\t\t\t\t}\n\t\t\t\tif (!hasGpuPowerData && (gpus[id].p !== undefined || gpus[id].pp !== undefined)) {\n\t\t\t\t\thasGpuPowerData = true\n\t\t\t\t}\n\t\t\t\tif (hasGpuEnginesData && hasGpuPowerData) break\n\t\t\t}\n\t\t}\n\t}\n\n\tconst isLinux = !(details?.os ?? system.info?.os)\n\tconst isPodman = details?.podman ?? system.info?.p ?? false\n\n\treturn (\n\t\t<>\n\t\t\t<div ref={chartWrapRef} className=\"grid gap-4 mb-14 overflow-x-clip\">\n\t\t\t\t{/* system info */}\n\t\t\t\t<InfoBar system={system} chartData={chartData} grid={grid} setGrid={setGrid} details={details} />\n\n\t\t\t\t{/* <Tabs defaultValue=\"overview\" className=\"w-full\">\n\t\t\t\t\t<TabsList className=\"w-full h-11\">\n\t\t\t\t\t\t<TabsTrigger value=\"overview\" className=\"w-full h-9\">Overview</TabsTrigger>\n\t\t\t\t\t\t<TabsTrigger value=\"containers\" className=\"w-full h-9\">Containers</TabsTrigger>\n\t\t\t\t\t\t<TabsTrigger value=\"smart\" className=\"w-full h-9\">S.M.A.R.T.</TabsTrigger>\n\t\t\t\t\t</TabsList>\n\t\t\t\t\t<TabsContent value=\"smart\">\n\t\t\t\t\t</TabsContent>\n\t\t\t\t</Tabs> */}\n\n\t\t\t\t{/* main charts */}\n\t\t\t\t<div className=\"grid xl:grid-cols-2 gap-4\">\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`CPU Usage`}\n\t\t\t\t\t\tdescription={t`Average system-wide CPU utilization`}\n\t\t\t\t\t\tcornerEl={\n\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t{maxValSelect}\n\t\t\t\t\t\t\t\t<CpuCoresSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: t`CPU Usage`,\n\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),\n\t\t\t\t\t\t\t\t\tcolor: 1,\n\t\t\t\t\t\t\t\t\topacity: 0.4,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\ttickFormatter={(val) => `${toFixedFloat(val, 2)}%`}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${decimalString(value)}%`}\n\t\t\t\t\t\t\tdomain={pinnedAxisDomain()}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t{containerFilterBar && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={dockerOrPodman(t`Docker CPU Usage`, isPodman)}\n\t\t\t\t\t\t\tdescription={t`Average CPU utilization of containers`}\n\t\t\t\t\t\t\tcornerEl={containerFilterBar}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ContainerChart\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tdataKey=\"c\"\n\t\t\t\t\t\t\t\tchartType={ChartType.CPU}\n\t\t\t\t\t\t\t\tchartConfig={containerChartConfigs.cpu}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Memory Usage`}\n\t\t\t\t\t\tdescription={t`Precise utilization at the recorded time`}\n\t\t\t\t\t\tcornerEl={maxValSelect}\n\t\t\t\t\t>\n\t\t\t\t\t\t<MemChart chartData={chartData} showMax={showMax} />\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t{containerFilterBar && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={dockerOrPodman(t`Docker Memory Usage`, isPodman)}\n\t\t\t\t\t\t\tdescription={dockerOrPodman(t`Memory usage of docker containers`, isPodman)}\n\t\t\t\t\t\t\tcornerEl={containerFilterBar}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ContainerChart\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tdataKey=\"m\"\n\t\t\t\t\t\t\t\tchartType={ChartType.Memory}\n\t\t\t\t\t\t\t\tchartConfig={containerChartConfigs.memory}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<ChartCard empty={dataEmpty} grid={grid} title={t`Disk Usage`} description={t`Usage of root partition`}>\n\t\t\t\t\t\t<DiskChart chartData={chartData} dataKey=\"stats.du\" diskSize={systemStats.at(-1)?.stats.d ?? NaN} />\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Disk I/O`}\n\t\t\t\t\t\tdescription={t`Throughput of root filesystem`}\n\t\t\t\t\t\tcornerEl={maxValSelect}\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: t({ message: \"Write\", comment: \"Disk write\" }),\n\t\t\t\t\t\t\t\t\tdataKey: ({ stats }: SystemStatsRecord) => {\n\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\treturn stats?.dio?.[1] ?? (stats?.dwm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn stats?.dio?.[1] ?? (stats?.dw ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tcolor: 3,\n\t\t\t\t\t\t\t\t\topacity: 0.3,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: t({ message: \"Read\", comment: \"Disk read\" }),\n\t\t\t\t\t\t\t\t\tdataKey: ({ stats }: SystemStatsRecord) => {\n\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\treturn stats?.diom?.[0] ?? (stats?.drm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn stats?.dio?.[0] ?? (stats?.dr ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tcolor: 1,\n\t\t\t\t\t\t\t\t\topacity: 0.3,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tshowTotal={true}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t<ChartCard\n\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\ttitle={t`Bandwidth`}\n\t\t\t\t\t\tcornerEl={\n\t\t\t\t\t\t\t<div className=\"flex gap-2\">\n\t\t\t\t\t\t\t\t{maxValSelect}\n\t\t\t\t\t\t\t\t<NetworkSheet chartData={chartData} dataEmpty={dataEmpty} grid={grid} maxValues={maxValues} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdescription={t`Network traffic of public interfaces`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: t`Sent`,\n\t\t\t\t\t\t\t\t\t// use bytes if available, otherwise multiply old MB (can remove in future)\n\t\t\t\t\t\t\t\t\tdataKey(data: SystemStatsRecord) {\n\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\treturn data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn data?.stats?.b?.[0] ?? (data?.stats?.ns ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tcolor: 5,\n\t\t\t\t\t\t\t\t\topacity: 0.2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tlabel: t`Received`,\n\t\t\t\t\t\t\t\t\tdataKey(data: SystemStatsRecord) {\n\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\treturn data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\treturn data?.stats?.b?.[1] ?? (data?.stats?.nr ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tcolor: 2,\n\t\t\t\t\t\t\t\t\topacity: 0.2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t// try to place the lesser number in front for better visibility\n\t\t\t\t\t\t\t\t.sort(() => (systemStats.at(-1)?.stats.b?.[1] ?? 0) - (systemStats.at(-1)?.stats.b?.[0] ?? 0))}\n\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tcontentFormatter={(data) => {\n\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)\n\t\t\t\t\t\t\t\treturn `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tshowTotal={true}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t{containerFilterBar && containerData.length > 0 && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={dockerOrPodman(t`Docker Network I/O`, isPodman)}\n\t\t\t\t\t\t\tdescription={dockerOrPodman(t`Network traffic of docker containers`, isPodman)}\n\t\t\t\t\t\t\tcornerEl={containerFilterBar}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ContainerChart\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tchartType={ChartType.Network}\n\t\t\t\t\t\t\t\tdataKey=\"n\"\n\t\t\t\t\t\t\t\tchartConfig={containerChartConfigs.network}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Swap chart */}\n\t\t\t\t\t{(systemStats.at(-1)?.stats.su ?? 0) > 0 && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`Swap Usage`}\n\t\t\t\t\t\t\tdescription={t`Swap space used by the system`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<SwapChart chartData={chartData} />\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Load Average chart */}\n\t\t\t\t\t{chartData.agentVersion?.minor > 12 && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`Load Average`}\n\t\t\t\t\t\t\tdescription={t`System load averages over time`}\n\t\t\t\t\t\t\tlegend={true}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<LoadAverageChart chartData={chartData} />\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Temperature chart */}\n\t\t\t\t\t{systemStats.at(-1)?.stats.t && (\n\t\t\t\t\t\t<div ref={temperatureChartRef} className={cn(\"odd:last-of-type:col-span-full\", { \"col-span-full\": !grid })}>\n\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\ttitle={t`Temperature`}\n\t\t\t\t\t\t\t\tdescription={t`Temperatures of system sensors`}\n\t\t\t\t\t\t\t\tcornerEl={<FilterBar store={$temperatureFilter} />}\n\t\t\t\t\t\t\t\tlegend={Object.keys(systemStats.at(-1)?.stats.t ?? {}).length < 12}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<TemperatureChart chartData={chartData} />\n\t\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{/* Battery chart */}\n\t\t\t\t\t{systemStats.at(-1)?.stats.bat && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`Battery`}\n\t\t\t\t\t\t\tdescription={`${t({\n\t\t\t\t\t\t\t\tmessage: \"Current state\",\n\t\t\t\t\t\t\t\tcomment: \"Context: Battery state\",\n\t\t\t\t\t\t\t})}: ${batteryStateTranslations[systemStats.at(-1)?.stats.bat?.[1] ?? 0]()}`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tlabel: t`Charge`,\n\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => stats?.bat?.[0],\n\t\t\t\t\t\t\t\t\t\tcolor: 1,\n\t\t\t\t\t\t\t\t\t\topacity: 0.35,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t\tdomain={[0, 100]}\n\t\t\t\t\t\t\t\ttickFormatter={(val) => `${val}%`}\n\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${value}%`}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\t\t\t\t\t{/* GPU power draw chart */}\n\t\t\t\t\t{hasGpuPowerData && (\n\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\ttitle={t`GPU Power Draw`}\n\t\t\t\t\t\t\tdescription={t`Average power consumption of GPUs`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<GpuPowerChart chartData={chartData} />\n\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\n\t\t\t\t{/* Non-power GPU charts */}\n\t\t\t\t{hasGpuData && (\n\t\t\t\t\t<div className=\"grid xl:grid-cols-2 gap-4\">\n\t\t\t\t\t\t{hasGpuEnginesData && (\n\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\tlegend={true}\n\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\ttitle={t`GPU Engines`}\n\t\t\t\t\t\t\t\tdescription={t`Average utilization of GPU engines`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<GpuEnginesChart chartData={chartData} />\n\t\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{lastGpus &&\n\t\t\t\t\t\t\tObject.keys(lastGpus).map((id) => {\n\t\t\t\t\t\t\t\tconst gpu = lastGpus[id] as GPUData\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<div key={id} className=\"contents\">\n\t\t\t\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(grid && \"!col-span-1\")}\n\t\t\t\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\t\t\t\ttitle={`${gpu.n} ${t`Usage`}`}\n\t\t\t\t\t\t\t\t\t\t\tdescription={t`Average utilization of ${gpu.n}`}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: t`Usage`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.35,\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t\t\t\t\t\ttickFormatter={(val) => `${toFixedFloat(val, 2)}%`}\n\t\t\t\t\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => `${decimalString(value)}%`}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t</ChartCard>\n\n\t\t\t\t\t\t\t\t\t\t{(gpu.mt ?? 0) > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\t\t\t\t\ttitle={`${gpu.n} VRAM`}\n\t\t\t\t\t\t\t\t\t\t\t\tdescription={t`Precise utilization at the recorded time`}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: t`Usage`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: 2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.25,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmax={gpu.mt}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, false, Unit.Bytes, true)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, false, Unit.Bytes, true)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue)} ${unit}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{/* extra filesystem charts */}\n\t\t\t\t{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).length > 0 && (\n\t\t\t\t\t<div className=\"grid xl:grid-cols-2 gap-4\">\n\t\t\t\t\t\t{Object.keys(systemStats.at(-1)?.stats.efs ?? {}).map((extraFsName) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={extraFsName} className=\"contents\">\n\t\t\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\t\t\ttitle={`${extraFsName} ${t`Usage`}`}\n\t\t\t\t\t\t\t\t\t\tdescription={t`Disk usage of ${extraFsName}`}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<DiskChart\n\t\t\t\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\t\t\t\tdataKey={({ stats }: SystemStatsRecord) => stats?.efs?.[extraFsName]?.du}\n\t\t\t\t\t\t\t\t\t\t\tdiskSize={systemStats.at(-1)?.stats.efs?.[extraFsName].d ?? NaN}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t\t\t\t\t<ChartCard\n\t\t\t\t\t\t\t\t\t\tempty={dataEmpty}\n\t\t\t\t\t\t\t\t\t\tgrid={grid}\n\t\t\t\t\t\t\t\t\t\ttitle={`${extraFsName} I/O`}\n\t\t\t\t\t\t\t\t\t\tdescription={t`Throughput of ${extraFsName}`}\n\t\t\t\t\t\t\t\t\t\tcornerEl={maxValSelect}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<AreaChartDefault\n\t\t\t\t\t\t\t\t\t\t\tchartData={chartData}\n\t\t\t\t\t\t\t\t\t\t\tdataPoints={[\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: t`Write`,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstats?.efs?.[extraFsName]?.wbm || (stats?.efs?.[extraFsName]?.wm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn stats?.efs?.[extraFsName]?.wb || (stats?.efs?.[extraFsName]?.w ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: 3,\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.3,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel: t`Read`,\n\t\t\t\t\t\t\t\t\t\t\t\t\tdataKey: ({ stats }) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (showMax) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstats?.efs?.[extraFsName]?.rbm ?? (stats?.efs?.[extraFsName]?.rm ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\treturn stats?.efs?.[extraFsName]?.rb ?? (stats?.efs?.[extraFsName]?.r ?? 0) * 1024 * 1024\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.3,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\t\t\t\t\tmaxToggled={maxValues}\n\t\t\t\t\t\t\t\t\t\t\ttickFormatter={(val) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitDisk, false)\n\t\t\t\t\t\t\t\t\t\t\t\treturn `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tcontentFormatter={({ value }) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)\n\t\t\t\t\t\t\t\t\t\t\t\treturn `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</ChartCard>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{compareSemVer(chartData.agentVersion, parseSemVer(\"0.15.0\")) >= 0 && <LazySmartTable systemId={system.id} />}\n\n\t\t\t\t{containerData.length > 0 && compareSemVer(chartData.agentVersion, parseSemVer(\"0.14.0\")) >= 0 && (\n\t\t\t\t\t<LazyContainersTable systemId={system.id} />\n\t\t\t\t)}\n\n\t\t\t\t{isLinux && compareSemVer(chartData.agentVersion, parseSemVer(\"0.16.0\")) >= 0 && (\n\t\t\t\t\t<LazySystemdTable systemId={system.id} />\n\t\t\t\t)}\n\t\t\t</div>\n\n\t\t\t{/* add space for tooltip if lots of sensors */}\n\t\t\t{bottomSpacing > 0 && <span className=\"block\" style={{ height: bottomSpacing }} />}\n\t\t</>\n\t)\n})\n\nfunction GpuEnginesChart({ chartData }: { chartData: ChartData }) {\n\tconst { gpuId, engines } = useMemo(() => {\n\t\tfor (let i = chartData.systemStats.length - 1; i >= 0; i--) {\n\t\t\tconst gpus = chartData.systemStats[i].stats?.g\n\t\t\tif (!gpus) continue\n\t\t\tfor (const id in gpus) {\n\t\t\t\tif (gpus[id].e) {\n\t\t\t\t\treturn { gpuId: id, engines: Object.keys(gpus[id].e).sort() }\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { gpuId: null, engines: [] }\n\t}, [chartData.systemStats])\n\n\tif (!gpuId) {\n\t\treturn null\n\t}\n\n\tconst dataPoints: DataPoint[] = engines.map((engine, i) => ({\n\t\tlabel: engine,\n\t\tdataKey: ({ stats }: SystemStatsRecord) => stats?.g?.[gpuId]?.e?.[engine] ?? 0,\n\t\tcolor: `hsl(${140 + (((i * 360) / engines.length) % 360)}, 65%, 52%)`,\n\t\topacity: 0.35,\n\t}))\n\n\treturn (\n\t\t<LineChartDefault\n\t\t\tlegend={true}\n\t\t\tchartData={chartData}\n\t\t\tdataPoints={dataPoints}\n\t\t\ttickFormatter={(val) => `${toFixedFloat(val, 2)}%`}\n\t\t\tcontentFormatter={({ value }) => `${decimalString(value)}%`}\n\t\t/>\n\t)\n}\n\nfunction FilterBar({ store = $containerFilter }: { store?: typeof $containerFilter }) {\n\tconst storeValue = useStore(store)\n\tconst [inputValue, setInputValue] = useState(storeValue)\n\tconst { t } = useLingui()\n\n\tuseEffect(() => {\n\t\tsetInputValue(storeValue)\n\t}, [storeValue])\n\n\tuseEffect(() => {\n\t\tif (inputValue === storeValue) {\n\t\t\treturn\n\t\t}\n\t\tconst handle = window.setTimeout(() => store.set(inputValue), 80)\n\t\treturn () => clearTimeout(handle)\n\t}, [inputValue, storeValue, store])\n\n\tconst handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n\t\tconst value = e.target.value\n\t\tsetInputValue(value)\n\t}, [])\n\n\tconst handleClear = useCallback(() => {\n\t\tsetInputValue(\"\")\n\t\tstore.set(\"\")\n\t}, [store])\n\n\treturn (\n\t\t<>\n\t\t\t<Input\n\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\tclassName=\"ps-4 pe-8 w-full sm:w-44\"\n\t\t\t\tonChange={handleChange}\n\t\t\t\tvalue={inputValue}\n\t\t\t/>\n\t\t\t{inputValue && (\n\t\t\t\t<Button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\taria-label=\"Clear\"\n\t\t\t\t\tclassName=\"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100\"\n\t\t\t\t\tonClick={handleClear}\n\t\t\t\t>\n\t\t\t\t\t<XIcon className=\"h-4 w-4\" />\n\t\t\t\t</Button>\n\t\t\t)}\n\t\t</>\n\t)\n}\n\nconst SelectAvgMax = memo(({ max }: { max: boolean }) => {\n\tconst Icon = max ? ChartMax : ChartAverage\n\treturn (\n\t\t<Select value={max ? \"max\" : \"avg\"} onValueChange={(e) => $maxValues.set(e === \"max\")}>\n\t\t\t<SelectTrigger className=\"relative ps-10 pe-5 w-full sm:w-44\">\n\t\t\t\t<Icon className=\"h-4 w-4 absolute start-4 top-1/2 -translate-y-1/2 opacity-85\" />\n\t\t\t\t<SelectValue />\n\t\t\t</SelectTrigger>\n\t\t\t<SelectContent>\n\t\t\t\t<SelectItem key=\"avg\" value=\"avg\">\n\t\t\t\t\t<Trans>Average</Trans>\n\t\t\t\t</SelectItem>\n\t\t\t\t<SelectItem key=\"max\" value=\"max\">\n\t\t\t\t\t<Trans comment=\"Chart select field. Please try to keep this short.\">Max 1 min</Trans>\n\t\t\t\t</SelectItem>\n\t\t\t</SelectContent>\n\t\t</Select>\n\t)\n})\n\nexport function ChartCard({\n\ttitle,\n\tdescription,\n\tchildren,\n\tgrid,\n\tempty,\n\tcornerEl,\n\tlegend,\n\tclassName,\n}: {\n\ttitle: string\n\tdescription: string\n\tchildren: React.ReactNode\n\tgrid?: boolean\n\tempty?: boolean\n\tcornerEl?: JSX.Element | null\n\tlegend?: boolean\n\tclassName?: string\n}) {\n\tconst { isIntersecting, ref } = useIntersectionObserver()\n\n\treturn (\n\t\t<Card\n\t\t\tclassName={cn(\"pb-2 sm:pb-4 odd:last-of-type:col-span-full min-h-full\", { \"col-span-full\": !grid }, className)}\n\t\t\tref={ref}\n\t\t>\n\t\t\t<CardHeader className=\"pb-5 pt-4 gap-1 relative max-sm:py-3 max-sm:px-4\">\n\t\t\t\t<CardTitle className=\"text-xl sm:text-2xl\">{title}</CardTitle>\n\t\t\t\t<CardDescription>{description}</CardDescription>\n\t\t\t\t{cornerEl && <div className=\"py-1 grid sm:justify-end sm:absolute sm:top-3.5 sm:end-3.5\">{cornerEl}</div>}\n\t\t\t</CardHeader>\n\t\t\t<div className={cn(\"ps-0 w-[calc(100%-1.3em)] relative group\", legend ? \"h-54 md:h-56\" : \"h-48 md:h-52\")}>\n\t\t\t\t{\n\t\t\t\t\t<Spinner\n\t\t\t\t\t\tmsg={empty ? t`Waiting for enough records to display` : undefined}\n\t\t\t\t\t\t// className=\"group-has-[.opacity-100]:opacity-0 transition-opacity\"\n\t\t\t\t\t\tclassName=\"group-has-[.opacity-100]:invisible duration-100\"\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\t{isIntersecting && children}\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n\nconst ContainersTable = lazy(() => import(\"../containers-table/containers-table\"))\n\nfunction LazyContainersTable({ systemId }: { systemId: string }) {\n\tconst { isIntersecting, ref } = useIntersectionObserver({ rootMargin: \"90px\" })\n\treturn (\n\t\t<div ref={ref} className={cn(isIntersecting && \"contents\")}>\n\t\t\t{isIntersecting && <ContainersTable systemId={systemId} />}\n\t\t</div>\n\t)\n}\n\nconst SmartTable = lazy(() => import(\"./system/smart-table\"))\n\nfunction LazySmartTable({ systemId }: { systemId: string }) {\n\tconst { isIntersecting, ref } = useIntersectionObserver({ rootMargin: \"90px\" })\n\treturn (\n\t\t<div ref={ref} className={cn(isIntersecting && \"contents\")}>\n\t\t\t{isIntersecting && <SmartTable systemId={systemId} />}\n\t\t</div>\n\t)\n}\n\nconst SystemdTable = lazy(() => import(\"../systemd-table/systemd-table\"))\n\nfunction LazySystemdTable({ systemId }: { systemId: string }) {\n\tconst { isIntersecting, ref } = useIntersectionObserver()\n\treturn (\n\t\t<div ref={ref} className={cn(isIntersecting && \"contents\")}>\n\t\t\t{isIntersecting && <SystemdTable systemId={systemId} />}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/spinner.tsx",
    "content": "import { LoaderCircleIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport default function ({ msg, className }: { msg?: string; className?: string }) {\n\treturn (\n\t\t<div className={cn(className, \"flex flex-col items-center justify-center h-full absolute inset-0\")}>\n\t\t\t{msg ? (\n\t\t\t\t<p className={\"opacity-60 mb-2 text-center text-sm px-4\"}>{msg}</p>\n\t\t\t) : (\n\t\t\t\t<LoaderCircleIcon className=\"animate-spin h-10 w-10 opacity-60\" />\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/systemd-table/systemd-table-columns.tsx",
    "content": "import type { Column, ColumnDef } from \"@tanstack/react-table\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn, decimalString, formatBytes, hourWithSeconds } from \"@/lib/utils\"\nimport type { SystemdRecord } from \"@/types\"\nimport { ServiceStatus, ServiceStatusLabels, ServiceSubState, ServiceSubStateLabels } from \"@/lib/enums\"\nimport {\n\tActivityIcon,\n\tArrowUpDownIcon,\n\tClockIcon,\n\tCpuIcon,\n\tMemoryStickIcon,\n\tTerminalSquareIcon,\n} from \"lucide-react\"\nimport { Badge } from \"../ui/badge\"\nimport { t } from \"@lingui/core/macro\"\n// import { $allSystemsById } from \"@/lib/stores\"\n// import { useStore } from \"@nanostores/react\"\n\nfunction getSubStateColor(subState: ServiceSubState) {\n\tswitch (subState) {\n\t\tcase ServiceSubState.Running:\n\t\t\treturn \"bg-green-500\"\n\t\tcase ServiceSubState.Failed:\n\t\t\treturn \"bg-red-500\"\n\t\tcase ServiceSubState.Dead:\n\t\t\treturn \"bg-yellow-500\"\n\t\tdefault:\n\t\t\treturn \"bg-zinc-500\"\n\t}\n}\n\n\nexport const systemdTableCols: ColumnDef<SystemdRecord>[] = [\n\t{\n\t\tid: \"name\",\n\t\tsortingFn: (a, b) => a.original.name.localeCompare(b.original.name),\n\t\taccessorFn: (record) => record.name,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Name`} Icon={TerminalSquareIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\treturn <span className=\"ms-1.5 xl:w-50 block truncate\">{getValue() as string}</span>\n\t\t},\n\t},\n\t// {\n\t// \tid: \"system\",\n\t// \taccessorFn: (record) => record.system,\n\t// \tsortingFn: (a, b) => {\n\t// \t\tconst allSystems = $allSystemsById.get()\n\t// \t\tconst systemNameA = allSystems[a.original.system]?.name ?? \"\"\n\t// \t\tconst systemNameB = allSystems[b.original.system]?.name ?? \"\"\n\t// \t\treturn systemNameA.localeCompare(systemNameB)\n\t// \t},\n\t// \theader: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,\n\t// \tcell: ({ getValue }) => {\n\t// \t\tconst allSystems = useStore($allSystemsById)\n\t// \t\treturn <span className=\"ms-1.5 xl:w-34 block truncate\">{allSystems[getValue() as string]?.name ?? \"\"}</span>\n\t// \t},\n\t// },\n\t{\n\t\tid: \"state\",\n\t\taccessorFn: (record) => record.state,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`State`} Icon={ActivityIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst statusValue = getValue() as ServiceStatus\n\t\t\tconst statusLabel = ServiceStatusLabels[statusValue] || \"Unknown\"\n\t\t\treturn (\n\t\t\t\t<Badge variant=\"outline\" className=\"dark:border-white/12\">\n\t\t\t\t\t<span className={cn(\"size-2 me-1.5 rounded-full\", getStatusColor(statusValue))} />\n\t\t\t\t\t{statusLabel}\n\t\t\t\t</Badge>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"sub\",\n\t\taccessorFn: (record) => record.sub,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Sub State`} Icon={ActivityIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst subState = getValue() as ServiceSubState\n\t\t\tconst subStateLabel = ServiceSubStateLabels[subState] || \"Unknown\"\n\t\t\treturn (\n\t\t\t\t<Badge variant=\"outline\" className=\"dark:border-white/12 text-xs capitalize\">\n\t\t\t\t\t<span className={cn(\"size-2 me-1.5 rounded-full\", getSubStateColor(subState))} />\n\t\t\t\t\t{subStateLabel}\n\t\t\t\t</Badge>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"cpu\",\n\t\taccessorFn: (record) => {\n\t\t\tif (record.sub !== ServiceSubState.Running) {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t\treturn record.cpu\n\t\t},\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={`${t`CPU`} (10m)`} Icon={CpuIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tif (val < 0) {\n\t\t\t\treturn <span className=\"ms-1.5 text-muted-foreground\">N/A</span>\n\t\t\t}\n\t\t\treturn <span className=\"ms-1.5 tabular-nums\">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"cpuPeak\",\n\t\taccessorFn: (record) => {\n\t\t\tif (record.sub !== ServiceSubState.Running) {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t\treturn record.cpuPeak ?? 0\n\t\t},\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`CPU Peak`} Icon={CpuIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tif (val < 0) {\n\t\t\t\treturn <span className=\"ms-1.5 text-muted-foreground\">N/A</span>\n\t\t\t}\n\t\t\treturn <span className=\"ms-1.5 tabular-nums\">{`${decimalString(val, val >= 10 ? 1 : 2)}%`}</span>\n\t\t},\n\t},\n\t{\n\t\tid: \"memory\",\n\t\taccessorFn: (record) => record.memory,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Memory`} Icon={MemoryStickIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tif (!val) {\n\t\t\t\treturn <span className=\"ms-1.5 text-muted-foreground\">N/A</span>\n\t\t\t}\n\t\t\tconst formatted = formatBytes(val, false, undefined, false)\n\t\t\treturn (\n\t\t\t\t<span className=\"ms-1.5 tabular-nums\">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"memPeak\",\n\t\taccessorFn: (record) => record.memPeak,\n\t\tinvertSorting: true,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Memory Peak`} Icon={MemoryStickIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst val = getValue() as number\n\t\t\tif (!val) {\n\t\t\t\treturn <span className=\"ms-1.5 text-muted-foreground\">N/A</span>\n\t\t\t}\n\t\t\tconst formatted = formatBytes(val, false, undefined, false)\n\t\t\treturn (\n\t\t\t\t<span className=\"ms-1.5 tabular-nums\">{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}</span>\n\t\t\t)\n\t\t},\n\t},\n\t{\n\t\tid: \"updated\",\n\t\tinvertSorting: true,\n\t\taccessorFn: (record) => record.updated,\n\t\theader: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,\n\t\tcell: ({ getValue }) => {\n\t\t\tconst timestamp = getValue() as number\n\t\t\treturn (\n\t\t\t\t<span className=\"ms-1.5 tabular-nums\">\n\t\t\t\t\t{hourWithSeconds(new Date(timestamp).toISOString())}\n\t\t\t\t</span>\n\t\t\t)\n\t\t},\n\t},\n]\n\nfunction HeaderButton({ column, name, Icon }: { column: Column<SystemdRecord>; name: string; Icon: React.ElementType }) {\n\tconst isSorted = column.getIsSorted()\n\treturn (\n\t\t<Button\n\t\t\tclassName={cn(\"h-9 px-3 flex items-center gap-2 duration-50\", isSorted && \"bg-accent/70 light:bg-accent text-accent-foreground/90\")}\n\t\t\tvariant=\"ghost\"\n\t\t\tonClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n\t\t>\n\t\t\t{Icon && <Icon className=\"size-4\" />}\n\t\t\t{name}\n\t\t\t<ArrowUpDownIcon className=\"size-4\" />\n\t\t</Button>\n\t)\n}\n\nexport function getStatusColor(status: ServiceStatus) {\n\tswitch (status) {\n\t\tcase ServiceStatus.Active:\n\t\t\treturn \"bg-green-500\"\n\t\tcase ServiceStatus.Failed:\n\t\t\treturn \"bg-red-500\"\n\t\tcase ServiceStatus.Reloading:\n\t\tcase ServiceStatus.Activating:\n\t\tcase ServiceStatus.Deactivating:\n\t\t\treturn \"bg-yellow-500\"\n\t\tdefault:\n\t\t\treturn \"bg-zinc-500\"\n\t}\n}"
  },
  {
    "path": "internal/site/src/components/systemd-table/systemd-table.tsx",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { Trans } from \"@lingui/react/macro\"\nimport {\n\ttype ColumnFiltersState,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetFilteredRowModel,\n\tgetSortedRowModel,\n\ttype Row,\n\ttype SortingState,\n\ttype Table as TableType,\n\tuseReactTable,\n\ttype VisibilityState,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer, type VirtualItem } from \"@tanstack/react-virtual\"\nimport { LoaderCircleIcon } from \"lucide-react\"\nimport { listenKeys } from \"nanostores\"\nimport { memo, type ReactNode, useEffect, useMemo, useRef, useState } from \"react\"\nimport { getStatusColor, systemdTableCols } from \"@/components/systemd-table/systemd-table-columns\"\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Card, CardDescription, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Sheet, SheetContent, SheetHeader, SheetTitle } from \"@/components/ui/sheet\"\nimport { TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { pb } from \"@/lib/api\"\nimport { ServiceStatus, ServiceStatusLabels, type ServiceSubState, ServiceSubStateLabels } from \"@/lib/enums\"\nimport { $allSystemsById } from \"@/lib/stores\"\nimport { cn, decimalString, formatBytes, useBrowserStorage } from \"@/lib/utils\"\nimport type { SystemdRecord, SystemdServiceDetails } from \"@/types\"\nimport { Separator } from \"../ui/separator\"\n\nexport default function SystemdTable({ systemId }: { systemId?: string }) {\n\tconst loadTime = Date.now()\n\tconst [data, setData] = useState<SystemdRecord[]>([])\n\tconst [sorting, setSorting] = useBrowserStorage<SortingState>(\n\t\t`sort-sd-${systemId ? 1 : 0}`,\n\t\t[{ id: systemId ? \"name\" : \"system\", desc: false }],\n\t\tsessionStorage\n\t)\n\tconst [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n\tconst [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})\n\tconst [globalFilter, setGlobalFilter] = useState(\"\")\n\n\t// clear old data when systemId changes\n\tuseEffect(() => {\n\t\treturn setData([])\n\t}, [systemId])\n\n\tuseEffect(() => {\n\t\tconst lastUpdated = data[0]?.updated ?? 0\n\n\t\tfunction fetchData(systemId?: string) {\n\t\t\tpb.collection<SystemdRecord>(\"systemd_services\")\n\t\t\t\t.getList(0, 2000, {\n\t\t\t\t\tfields: \"name,state,sub,cpu,cpuPeak,memory,memPeak,updated\",\n\t\t\t\t\tfilter: systemId ? pb.filter(\"system={:system}\", { system: systemId }) : undefined,\n\t\t\t\t})\n\t\t\t\t.then(\n\t\t\t\t\t({ items }) =>\n\t\t\t\t\t\titems.length &&\n\t\t\t\t\t\tsetData((curItems) => {\n\t\t\t\t\t\t\tconst lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)\n\t\t\t\t\t\t\tconst systemdNames = new Set()\n\t\t\t\t\t\t\tconst newItems: SystemdRecord[] = []\n\t\t\t\t\t\t\tfor (const item of items) {\n\t\t\t\t\t\t\t\tif (Math.abs(lastUpdated - item.updated) < 70_000) {\n\t\t\t\t\t\t\t\t\tsystemdNames.add(item.name)\n\t\t\t\t\t\t\t\t\tnewItems.push(item)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfor (const item of curItems) {\n\t\t\t\t\t\t\t\tif (!systemdNames.has(item.name) && lastUpdated - item.updated < 70_000) {\n\t\t\t\t\t\t\t\t\tnewItems.push(item)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn newItems\n\t\t\t\t\t\t})\n\t\t\t\t)\n\t\t}\n\n\t\t// initial load\n\t\tfetchData(systemId)\n\n\t\t// if no systemId, pull system containers after every system update\n\t\tif (!systemId) {\n\t\t\treturn $allSystemsById.listen((_value, _oldValue, systemId) => {\n\t\t\t\t// exclude initial load of systems\n\t\t\t\tif (Date.now() - loadTime > 500) {\n\t\t\t\t\tfetchData(systemId)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\t// if systemId, fetch containers after the system is updated\n\t\treturn listenKeys($allSystemsById, [systemId], (_newSystems) => {\n\t\t\t// don't fetch data if the last update is less than 9.5 minutes\n\t\t\tif (lastUpdated > Date.now() - 9.5 * 60 * 1000) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfetchData(systemId)\n\t\t})\n\t}, [systemId])\n\n\tconst table = useReactTable({\n\t\tdata,\n\t\t// columns: systemdTableCols.filter((col) => (systemId ? col.id !== \"system\" : true)),\n\t\tcolumns: systemdTableCols,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tonSortingChange: setSorting,\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tonColumnVisibilityChange: setColumnVisibility,\n\t\tdefaultColumn: {\n\t\t\tsortUndefined: \"last\",\n\t\t\tsize: 100,\n\t\t\tminSize: 0,\n\t\t},\n\t\tstate: {\n\t\t\tsorting,\n\t\t\tcolumnFilters,\n\t\t\tcolumnVisibility,\n\t\t\tglobalFilter,\n\t\t},\n\t\tonGlobalFilterChange: setGlobalFilter,\n\t\tglobalFilterFn: (row, _columnId, filterValue) => {\n\t\t\tconst service = row.original\n\t\t\tconst systemName = $allSystemsById.get()[service.system]?.name ?? \"\"\n\t\t\tconst name = service.name ?? \"\"\n\t\t\tconst statusLabel = ServiceStatusLabels[service.state as ServiceStatus] ?? \"\"\n\t\t\tconst subState = service.sub ?? \"\"\n\t\t\tconst searchString = `${systemName} ${name} ${statusLabel} ${subState}`.toLowerCase()\n\n\t\t\treturn (filterValue as string)\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(\" \")\n\t\t\t\t.every((term) => searchString.includes(term))\n\t\t},\n\t})\n\n\tconst rows = table.getRowModel().rows\n\tconst visibleColumns = table.getVisibleLeafColumns()\n\n\tconst statusTotals = useMemo(() => {\n\t\tconst totals = [0, 0, 0, 0, 0, 0]\n\t\tfor (const service of data) {\n\t\t\ttotals[service.state]++\n\t\t}\n\t\treturn totals\n\t}, [data])\n\n\tif (!data.length && !globalFilter) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<Card className=\"p-6 @container w-full\">\n\t\t\t<CardHeader className=\"p-0 mb-4\">\n\t\t\t\t<div className=\"grid md:flex gap-5 w-full items-end\">\n\t\t\t\t\t<div className=\"px-2 sm:px-1\">\n\t\t\t\t\t\t<CardTitle className=\"mb-2\">\n\t\t\t\t\t\t\t<Trans>Systemd Services</Trans>\n\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t<CardDescription className=\"flex items-center\">\n\t\t\t\t\t\t\t<Trans>Total: {data.length}</Trans>\n\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-4 mx-2 bg-primary/40\" />\n\t\t\t\t\t\t\t<Trans>Failed: {statusTotals[ServiceStatus.Failed]}</Trans>\n\t\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"h-4 mx-2 bg-primary/40\" />\n\t\t\t\t\t\t\t<Trans>Updated every 10 minutes.</Trans>\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Input\n\t\t\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\t\t\tvalue={globalFilter}\n\t\t\t\t\t\tonChange={(e) => setGlobalFilter(e.target.value)}\n\t\t\t\t\t\tclassName=\"ms-auto px-4 w-full max-w-full md:w-64\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</CardHeader>\n\t\t\t<div className=\"rounded-md\">\n\t\t\t\t<AllSystemdTable table={table} rows={rows} colLength={visibleColumns.length} systemId={systemId} />\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n\nconst AllSystemdTable = memo(function AllSystemdTable({\n\ttable,\n\trows,\n\tcolLength,\n\tsystemId,\n}: {\n\ttable: TableType<SystemdRecord>\n\trows: Row<SystemdRecord>[]\n\tcolLength: number\n\tsystemId?: string\n}) {\n\t// The virtualizer will need a reference to the scrollable container element\n\tconst scrollRef = useRef<HTMLDivElement>(null)\n\tconst activeService = useRef<SystemdRecord | null>(null)\n\tconst [sheetOpen, setSheetOpen] = useState(false)\n\tconst openSheet = (service: SystemdRecord) => {\n\t\tactiveService.current = service\n\t\tsetSheetOpen(true)\n\t}\n\n\tconst virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n\t\tcount: rows.length,\n\t\testimateSize: () => 54,\n\t\tgetScrollElement: () => scrollRef.current,\n\t\toverscan: 5,\n\t})\n\tconst virtualRows = virtualizer.getVirtualItems()\n\n\tconst paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)\n\tconst paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md\",\n\t\t\t\t// don't set min height if there are less than 2 rows, do set if we need to display the empty state\n\t\t\t\t(!rows.length || rows.length > 2) && \"min-h-50\"\n\t\t\t)}\n\t\t\tref={scrollRef}\n\t\t>\n\t\t\t{/* add header height to table size */}\n\t\t\t<div style={{ height: `${virtualizer.getTotalSize() + 48}px`, paddingTop, paddingBottom }}>\n\t\t\t\t<table className=\"text-sm w-full h-full text-nowrap\">\n\t\t\t\t\t<SystemdTableHead table={table} />\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{rows.length ? (\n\t\t\t\t\t\t\tvirtualRows.map((virtualRow) => {\n\t\t\t\t\t\t\t\tconst row = rows[virtualRow.index]\n\t\t\t\t\t\t\t\treturn <SystemdTableRow key={row.id} row={row} virtualRow={virtualRow} openSheet={openSheet} />\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t<TableCell colSpan={colLength} className=\"h-37 text-center pointer-events-none\">\n\t\t\t\t\t\t\t\t\t<Trans>No results.</Trans>\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t\t<SystemdSheet\n\t\t\t\tsheetOpen={sheetOpen}\n\t\t\t\tsetSheetOpen={setSheetOpen}\n\t\t\t\tactiveService={activeService}\n\t\t\t\tsystemId={systemId}\n\t\t\t/>\n\t\t</div>\n\t)\n})\n\nfunction SystemdSheet({\n\tsheetOpen,\n\tsetSheetOpen,\n\tactiveService,\n\tsystemId,\n}: {\n\tsheetOpen: boolean\n\tsetSheetOpen: (open: boolean) => void\n\tactiveService: React.RefObject<SystemdRecord | null>\n\tsystemId?: string\n}) {\n\tconst service = activeService.current\n\tconst [details, setDetails] = useState<SystemdServiceDetails | null>(null)\n\tconst [isLoading, setIsLoading] = useState(false)\n\tconst [error, setError] = useState<string | null>(null)\n\n\tuseEffect(() => {\n\t\tif (!sheetOpen || !service) {\n\t\t\treturn\n\t\t}\n\n\t\tsetError(null)\n\n\t\tlet cancelled = false\n\t\tsetDetails(null)\n\t\tsetIsLoading(true)\n\n\t\tpb.send<{ details: SystemdServiceDetails }>(\"/api/beszel/systemd/info\", {\n\t\t\tquery: {\n\t\t\t\tsystem: systemId,\n\t\t\t\tservice: service.name,\n\t\t\t},\n\t\t})\n\t\t\t.then(({ details }) => {\n\t\t\t\tif (cancelled) return\n\t\t\t\tif (details) {\n\t\t\t\t\tsetDetails(details)\n\t\t\t\t} else {\n\t\t\t\t\tsetDetails(null)\n\t\t\t\t\tsetError(t`No results found.`)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tif (cancelled) return\n\t\t\t\tsetError(err?.message ?? \"Failed to load service details\")\n\t\t\t\tsetDetails(null)\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tif (!cancelled) {\n\t\t\t\t\tsetIsLoading(false)\n\t\t\t\t}\n\t\t\t})\n\n\t\treturn () => {\n\t\t\tcancelled = true\n\t\t}\n\t}, [sheetOpen, service, systemId])\n\n\tif (!service) return null\n\n\tconst statusLabel = ServiceStatusLabels[service.state as ServiceStatus] ?? \"\"\n\tconst subStateLabel = ServiceSubStateLabels[service.sub as ServiceSubState] ?? \"\"\n\n\tconst notAvailable = <span className=\"text-muted-foreground\">N/A</span>\n\n\tconst formatMemory = (value?: number | null) => {\n\t\tif (value === undefined || value === null) {\n\t\t\treturn value === null ? t`Unlimited` : undefined\n\t\t}\n\t\tconst { value: convertedValue, unit } = formatBytes(value, false, undefined, false)\n\t\tconst digits = convertedValue >= 10 ? 1 : 2\n\t\treturn `${decimalString(convertedValue, digits)} ${unit}`\n\t}\n\n\tconst formatCpuTime = (ns?: number) => {\n\t\tif (!ns) return undefined\n\t\tconst seconds = ns / 1_000_000_000\n\t\tif (seconds >= 3600) {\n\t\t\tconst hours = Math.floor(seconds / 3600)\n\t\t\tconst minutes = Math.floor((seconds % 3600) / 60)\n\t\t\tconst secs = Math.floor(seconds % 60)\n\t\t\treturn [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null, secs ? `${secs}s` : null]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\" \")\n\t\t}\n\t\tif (seconds >= 60) {\n\t\t\tconst minutes = Math.floor(seconds / 60)\n\t\t\tconst secs = Math.floor(seconds % 60)\n\t\t\treturn `${minutes}m ${secs}s`\n\t\t}\n\t\tif (seconds >= 1) {\n\t\t\treturn `${decimalString(seconds, 2)}s`\n\t\t}\n\t\treturn `${decimalString(seconds * 1000, 2)}ms`\n\t}\n\n\tconst formatTasks = (current?: number, max?: number) => {\n\t\tconst hasCurrent = typeof current === \"number\" && current >= 0\n\t\tconst hasMax = typeof max === \"number\" && max > 0 && max !== null\n\t\tif (!hasCurrent && !hasMax) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{hasCurrent ? current : notAvailable}\n\t\t\t\t{hasMax && <span className=\"text-muted-foreground ms-1.5\">{`(${t`limit`}: ${max})`}</span>}\n\t\t\t\t{max === null && (\n\t\t\t\t\t<span className=\"text-muted-foreground ms-1.5\">{`(${t`limit`}: ${t`Unlimited`.toLowerCase()})`}</span>\n\t\t\t\t)}\n\t\t\t</>\n\t\t)\n\t}\n\n\tconst formatTimestamp = (timestamp?: number) => {\n\t\tif (!timestamp) return undefined\n\t\t// systemd timestamps are in microseconds, convert to milliseconds for JavaScript Date\n\t\tconst date = new Date(timestamp / 1000)\n\t\tif (Number.isNaN(date.getTime())) return undefined\n\t\treturn date.toLocaleString()\n\t}\n\n\tconst activeStateValue = (() => {\n\t\tconst stateText = details?.ActiveState\n\t\t\t? details.SubState\n\t\t\t\t? `${details.ActiveState} (${details.SubState})`\n\t\t\t\t: details.ActiveState\n\t\t\t: subStateLabel\n\t\t\t\t? `${statusLabel} (${subStateLabel})`\n\t\t\t\t: statusLabel\n\n\t\tfor (const [index, status] of ServiceStatusLabels.entries()) {\n\t\t\tif (details?.ActiveState?.toLowerCase() === status.toLowerCase()) {\n\t\t\t\tservice.state = index as ServiceStatus\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn (\n\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t<div className={cn(\"w-2 h-2 rounded-full flex-shrink-0\", getStatusColor(service.state))} />\n\t\t\t\t{stateText}\n\t\t\t</div>\n\t\t)\n\t})()\n\n\tconst statusTextValue = details?.Result\n\n\tconst cpuTime = formatCpuTime(details?.CPUUsageNSec)\n\tconst tasks = formatTasks(details?.TasksCurrent, details?.TasksMax)\n\tconst memoryCurrent = formatMemory(details?.MemoryCurrent)\n\tconst memoryPeak = formatMemory(details?.MemoryPeak)\n\tconst memoryLimit = formatMemory(details?.MemoryLimit)\n\tconst restartsValue = typeof details?.NRestarts === \"number\" ? details.NRestarts : undefined\n\tconst mainPidValue = typeof details?.MainPID === \"number\" && details.MainPID > 0 ? details.MainPID : undefined\n\tconst execMainPidValue =\n\t\ttypeof details?.ExecMainPID === \"number\" && details.ExecMainPID > 0 && details.ExecMainPID !== details?.MainPID\n\t\t\t? details.ExecMainPID\n\t\t\t: undefined\n\tconst activeEnterTimestamp = formatTimestamp(details?.ActiveEnterTimestamp)\n\tconst activeExitTimestamp = formatTimestamp(details?.ActiveExitTimestamp)\n\tconst inactiveEnterTimestamp = formatTimestamp(details?.InactiveEnterTimestamp)\n\tconst execMainStartTimestamp = undefined // Property not available in current systemd interface\n\n\tconst renderRow = (key: string, label: ReactNode, value?: ReactNode, alwaysShow = false) => {\n\t\tif (!alwaysShow && (value === undefined || value === null || value === \"\")) {\n\t\t\treturn null\n\t\t}\n\t\treturn (\n\t\t\t<tr key={key} className=\"border-b last:border-b-0\">\n\t\t\t\t<td className=\"px-3 py-2 font-medium bg-muted dark:bg-muted/40 align-top w-35\">{label}</td>\n\t\t\t\t<td className=\"px-3 py-2\">{value ?? notAvailable}</td>\n\t\t\t</tr>\n\t\t)\n\t}\n\n\tconst capitalize = (str: string) => `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`\n\n\treturn (\n\t\t<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>\n\t\t\t<SheetContent className=\"w-full sm:max-w-220 p-6 overflow-y-auto\">\n\t\t\t\t<SheetHeader className=\"p-0\">\n\t\t\t\t\t<SheetTitle>\n\t\t\t\t\t\t<Trans>Service Details</Trans>\n\t\t\t\t\t</SheetTitle>\n\t\t\t\t</SheetHeader>\n\t\t\t\t<div className=\"grid gap-6\">\n\t\t\t\t\t{isLoading && (\n\t\t\t\t\t\t<div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t<LoaderCircleIcon className=\"size-4 animate-spin\" />\n\t\t\t\t\t\t\t<Trans>Loading...</Trans>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{error && (\n\t\t\t\t\t\t<Alert className=\"border-destructive/50 text-destructive dark:border-destructive/60 dark:text-destructive\">\n\t\t\t\t\t\t\t<AlertTitle>\n\t\t\t\t\t\t\t\t<Trans>Error</Trans>\n\t\t\t\t\t\t\t</AlertTitle>\n\t\t\t\t\t\t\t<AlertDescription>{error}</AlertDescription>\n\t\t\t\t\t\t</Alert>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div className=\"border rounded-md\">\n\t\t\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t{renderRow(\"name\", t`Name`, service.name, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"description\", t`Description`, details?.Description, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"loadState\", t`Load state`, details?.LoadState, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"bootState\",\n\t\t\t\t\t\t\t\t\t\tt`Boot state`,\n\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center\">\n\t\t\t\t\t\t\t\t\t\t\t{details?.UnitFileState}\n\t\t\t\t\t\t\t\t\t\t\t{details?.UnitFilePreset && (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground ms-1.5\">(preset: {details?.UnitFilePreset})</span>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</div>,\n\t\t\t\t\t\t\t\t\t\ttrue\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"unitFile\", t`Unit file`, details?.FragmentPath, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"active\", t`Active state`, activeStateValue, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"status\", t`Status`, statusTextValue, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"documentation\",\n\t\t\t\t\t\t\t\t\t\tt`Documentation`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Documentation) && details.Documentation.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.Documentation.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<h3 className=\"text-sm font-medium mb-3\">\n\t\t\t\t\t\t\t<Trans>Runtime Metrics</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<div className=\"border rounded-md\">\n\t\t\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t{renderRow(\"mainPid\", t`Main PID`, mainPidValue, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"execMainPid\", t`Exec main PID`, execMainPidValue)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"tasks\", t`Tasks`, tasks, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"cpuTime\", t`CPU time`, cpuTime)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"memory\", t`Memory`, memoryCurrent, true)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"memoryPeak\", capitalize(t`Memory Peak`), memoryPeak)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"memoryLimit\", t`Memory limit`, memoryLimit)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"restarts\", t`Restarts`, restartsValue, true)}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"hidden has-[tr]:block\">\n\t\t\t\t\t\t<h3 className=\"text-sm font-medium mb-3\">\n\t\t\t\t\t\t\t<Trans>Relationships</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<div className=\"border rounded-md\">\n\t\t\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"wants\",\n\t\t\t\t\t\t\t\t\t\tt`Wants`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Wants) && details.Wants.length > 0 ? details.Wants.join(\", \") : undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"requires\",\n\t\t\t\t\t\t\t\t\t\tt`Requires`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Requires) && details.Requires.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.Requires.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"requiredBy\",\n\t\t\t\t\t\t\t\t\t\tt`Required by`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.RequiredBy) && details.RequiredBy.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.RequiredBy.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"conflicts\",\n\t\t\t\t\t\t\t\t\t\tt`Conflicts`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Conflicts) && details.Conflicts.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.Conflicts.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"before\",\n\t\t\t\t\t\t\t\t\t\tt`Before`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Before) && details.Before.length > 0 ? details.Before.join(\", \") : undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"after\",\n\t\t\t\t\t\t\t\t\t\tt`After`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.After) && details.After.length > 0 ? details.After.join(\", \") : undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"triggers\",\n\t\t\t\t\t\t\t\t\t\tt`Triggers`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.Triggers) && details.Triggers.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.Triggers.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t{renderRow(\n\t\t\t\t\t\t\t\t\t\t\"triggeredBy\",\n\t\t\t\t\t\t\t\t\t\tt`Triggered by`,\n\t\t\t\t\t\t\t\t\t\tArray.isArray(details?.TriggeredBy) && details.TriggeredBy.length > 0\n\t\t\t\t\t\t\t\t\t\t\t? details.TriggeredBy.join(\", \")\n\t\t\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"hidden has-[tr]:block\">\n\t\t\t\t\t\t<h3 className=\"text-sm font-medium mb-3\">\n\t\t\t\t\t\t\t<Trans>Lifecycle</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<div className=\"border rounded-md\">\n\t\t\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t{renderRow(\"activeSince\", t`Became active`, activeEnterTimestamp)}\n\t\t\t\t\t\t\t\t\t{service.state !== ServiceStatus.Active &&\n\t\t\t\t\t\t\t\t\t\trenderRow(\"lastActive\", t`Exited active`, activeExitTimestamp)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"inactiveSince\", t`Became inactive`, inactiveEnterTimestamp)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"execMainStart\", t`Process started`, execMainStartTimestamp)}\n\t\t\t\t\t\t\t\t\t{/* {renderRow(\"invocationId\", t`Invocation ID`, details?.InvocationID)} */}\n\t\t\t\t\t\t\t\t\t{/* {renderRow(\"freezerState\", t`Freezer State`, details?.FreezerState)} */}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"hidden has-[tr]:block\">\n\t\t\t\t\t\t<h3 className=\"text-sm font-medium mb-3\">\n\t\t\t\t\t\t\t<Trans>Capabilities</Trans>\n\t\t\t\t\t\t</h3>\n\t\t\t\t\t\t<div className=\"border rounded-md\">\n\t\t\t\t\t\t\t<table className=\"w-full text-sm\">\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t{renderRow(\"canStart\", t`Can start`, details?.CanStart ? t`Yes` : t`No`)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"canStop\", t`Can stop`, details?.CanStop ? t`Yes` : t`No`)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"canReload\", t`Can reload`, details?.CanReload ? t`Yes` : t`No`)}\n\t\t\t\t\t\t\t\t\t{/* {renderRow(\"refuseManualStart\", t`Refuse Manual Start`, details?.RefuseManualStart ? t`Yes` : t`No`)}\n\t\t\t\t\t\t\t\t\t{renderRow(\"refuseManualStop\", t`Refuse Manual Stop`, details?.RefuseManualStop ? t`Yes` : t`No`)} */}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</SheetContent>\n\t\t</Sheet>\n\t)\n}\n\nfunction SystemdTableHead({ table }: { table: TableType<SystemdRecord> }) {\n\treturn (\n\t\t<TableHeader className=\"sticky top-0 z-50 w-full border-b-2\">\n\t\t\t<div className=\"absolute -top-2 left-0 w-full h-4 bg-table-header z-50\"></div>\n\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t<tr key={headerGroup.id}>\n\t\t\t\t\t{headerGroup.headers.map((header) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<TableHead className=\"px-2\" key={header.id}>\n\t\t\t\t\t\t\t\t{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t</tr>\n\t\t\t))}\n\t\t</TableHeader>\n\t)\n}\n\nconst SystemdTableRow = memo(function SystemdTableRow({\n\trow,\n\tvirtualRow,\n\topenSheet,\n}: {\n\trow: Row<SystemdRecord>\n\tvirtualRow: VirtualItem\n\topenSheet: (service: SystemdRecord) => void\n}) {\n\treturn (\n\t\t<TableRow\n\t\t\tdata-state={row.getIsSelected() && \"selected\"}\n\t\t\tclassName=\"cursor-pointer transition-opacity\"\n\t\t\tonClick={() => openSheet(row.original)}\n\t\t>\n\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t<TableCell\n\t\t\t\t\tkey={cell.id}\n\t\t\t\t\tclassName=\"py-0\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\theight: virtualRow.size,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t</TableCell>\n\t\t\t))}\n\t\t</TableRow>\n\t)\n})\n"
  },
  {
    "path": "internal/site/src/components/systems-table/systems-table-columns.tsx",
    "content": "/** biome-ignore-all lint/correctness/useHookAtTopLevel: Hooks live inside memoized column definitions */\nimport { t } from \"@lingui/core/macro\"\nimport { Trans, useLingui } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport type { CellContext, ColumnDef, HeaderContext } from \"@tanstack/react-table\"\nimport type { ClassValue } from \"clsx\"\nimport {\n\tArrowUpDownIcon,\n\tChevronRightSquareIcon,\n\tClockArrowUp,\n\tCopyIcon,\n\tCpuIcon,\n\tHardDriveIcon,\n\tMemoryStickIcon,\n\tMoreHorizontalIcon,\n\tPauseCircleIcon,\n\tPenBoxIcon,\n\tPlayCircleIcon,\n\tServerIcon,\n\tTerminalSquareIcon,\n\tTrash2Icon,\n\tWifiIcon,\n} from \"lucide-react\"\nimport { memo, useMemo, useRef, useState } from \"react\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\"\nimport { isReadOnlyUser, pb } from \"@/lib/api\"\nimport { BatteryState, ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from \"@/lib/enums\"\nimport { $longestSystemNameLen, $userSettings } from \"@/lib/stores\"\nimport {\n\tcn,\n\tcopyToClipboard,\n\tdecimalString,\n\tformatBytes,\n\tformatTemperature,\n\tparseSemVer,\n\tsecondsToUptimeString,\n} from \"@/lib/utils\"\nimport { batteryStateTranslations } from \"@/lib/i18n\"\nimport type { SystemRecord } from \"@/types\"\nimport { SystemDialog } from \"../add-system\"\nimport AlertButton from \"../alerts/alert-button\"\nimport { $router, Link } from \"../router\"\nimport {\n\tAlertDialog,\n\tAlertDialogAction,\n\tAlertDialogCancel,\n\tAlertDialogContent,\n\tAlertDialogDescription,\n\tAlertDialogFooter,\n\tAlertDialogHeader,\n\tAlertDialogTitle,\n} from \"../ui/alert-dialog\"\nimport { Button, buttonVariants } from \"../ui/button\"\nimport { Dialog } from \"../ui/dialog\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"../ui/dropdown-menu\"\nimport {\n\tBatteryMediumIcon,\n\tEthernetIcon,\n\tGpuIcon,\n\tHourglassIcon,\n\tThermometerIcon,\n\tWebSocketIcon,\n\tBatteryHighIcon,\n\tBatteryLowIcon,\n\tPlugChargingIcon,\n\tBatteryFullIcon,\n} from \"../ui/icons\"\n\nconst STATUS_COLORS = {\n\t[SystemStatus.Up]: \"bg-green-500\",\n\t[SystemStatus.Down]: \"bg-red-500\",\n\t[SystemStatus.Paused]: \"bg-primary/40\",\n\t[SystemStatus.Pending]: \"bg-yellow-500\",\n} as const\n\nfunction getMeterStateByThresholds(value: number, warn = 65, crit = 90): MeterState {\n\treturn value >= crit ? MeterState.Crit : value >= warn ? MeterState.Warn : MeterState.Good\n}\n\n/**\n * @param viewMode - \"table\" or \"grid\"\n * @returns - Column definitions for the systems table\n */\nexport function SystemsTableColumns(viewMode: \"table\" | \"grid\"): ColumnDef<SystemRecord>[] {\n\treturn [\n\t\t{\n\t\t\t// size: 200,\n\t\t\tsize: 100,\n\t\t\tminSize: 0,\n\t\t\taccessorKey: \"name\",\n\t\t\tid: \"system\",\n\t\t\tname: () => t`System`,\n\t\t\tsortingFn: (a, b) => a.original.name.localeCompare(b.original.name),\n\t\t\tfilterFn: (() => {\n\t\t\t\tlet filterInput = \"\"\n\t\t\t\tlet filterInputLower = \"\"\n\t\t\t\tconst nameCache = new Map<string, string>()\n\t\t\t\tconst statusTranslations = {\n\t\t\t\t\t[SystemStatus.Up]: t`Up`.toLowerCase(),\n\t\t\t\t\t[SystemStatus.Down]: t`Down`.toLowerCase(),\n\t\t\t\t\t[SystemStatus.Paused]: t`Paused`.toLowerCase(),\n\t\t\t\t} as const\n\n\t\t\t\t// match filter value against name or translated status\n\t\t\t\treturn (row, _, newFilterInput) => {\n\t\t\t\t\tconst { name, status } = row.original\n\t\t\t\t\tif (newFilterInput !== filterInput) {\n\t\t\t\t\t\tfilterInput = newFilterInput\n\t\t\t\t\t\tfilterInputLower = newFilterInput.toLowerCase()\n\t\t\t\t\t}\n\t\t\t\t\tlet nameLower = nameCache.get(name)\n\t\t\t\t\tif (nameLower === undefined) {\n\t\t\t\t\t\tnameLower = name.toLowerCase()\n\t\t\t\t\t\tnameCache.set(name, nameLower)\n\t\t\t\t\t}\n\t\t\t\t\tif (nameLower.includes(filterInputLower)) {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tconst statusLower = statusTranslations[status as keyof typeof statusTranslations]\n\t\t\t\t\treturn statusLower?.includes(filterInputLower) || false\n\t\t\t\t}\n\t\t\t})(),\n\t\t\tenableHiding: false,\n\t\t\tinvertSorting: false,\n\t\t\tIcon: ServerIcon,\n\t\t\tcell: (info) => {\n\t\t\t\tconst { name, id } = info.row.original\n\t\t\t\tconst longestName = useStore($longestSystemNameLen)\n\t\t\t\tconst linkUrl = getPagePath($router, \"system\", { id })\n\n\t\t\t\treturn (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<span className=\"flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1\">\n\t\t\t\t\t\t\t<IndicatorDot system={info.row.original} />\n\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\thref={linkUrl}\n\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\tclassName=\"truncate z-10 relative\"\n\t\t\t\t\t\t\t\tstyle={{ width: `${longestName / 1.05}ch` }}\n\t\t\t\t\t\t\t\tonMouseEnter={(e) => {\n\t\t\t\t\t\t\t\t\t// set title on hover if text is truncated to show full name\n\t\t\t\t\t\t\t\t\tconst a = e.currentTarget\n\t\t\t\t\t\t\t\t\tif (a.scrollWidth > a.clientWidth) {\n\t\t\t\t\t\t\t\t\t\ta.title = name\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\ta.removeAttribute(\"title\")\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{name}\n\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<Link href={linkUrl} className=\"inset-0 absolute size-full\" aria-label={name}></Link>\n\t\t\t\t\t</>\n\t\t\t\t)\n\t\t\t},\n\t\t\theader: sortableHeader,\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.cpu || undefined,\n\t\t\tid: \"cpu\",\n\t\t\tname: () => t`CPU`,\n\t\t\tcell: TableCellWithMeter,\n\t\t\tIcon: CpuIcon,\n\t\t\theader: sortableHeader,\n\t\t},\n\t\t{\n\t\t\t// accessorKey: \"info.mp\",\n\t\t\taccessorFn: ({ info }) => info.mp || undefined,\n\t\t\tid: \"memory\",\n\t\t\tname: () => t`Memory`,\n\t\t\tcell: TableCellWithMeter,\n\t\t\tIcon: MemoryStickIcon,\n\t\t\theader: sortableHeader,\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.dp || undefined,\n\t\t\tid: \"disk\",\n\t\t\tname: () => t`Disk`,\n\t\t\tcell: (info: CellContext<SystemRecord, unknown>) =>\n\t\t\t\tinfo.row.original.info.efs ? DiskCellWithMultiple(info) : TableCellWithMeter(info),\n\t\t\tIcon: HardDriveIcon,\n\t\t\theader: sortableHeader,\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.g || undefined,\n\t\t\tid: \"gpu\",\n\t\t\tname: () => \"GPU\",\n\t\t\tcell: TableCellWithMeter,\n\t\t\tIcon: GpuIcon,\n\t\t\theader: sortableHeader,\n\t\t},\n\t\t{\n\t\t\tid: \"loadAverage\",\n\t\t\taccessorFn: ({ info }) => info.la?.reduce((acc, curr) => acc + curr, 0),\n\t\t\tname: () => t({ message: \"Load Avg\", comment: \"Short label for load average\" }),\n\t\t\tsize: 0,\n\t\t\tIcon: HourglassIcon,\n\t\t\theader: sortableHeader,\n\t\t\tcell(info: CellContext<SystemRecord, unknown>) {\n\t\t\t\tconst { info: sysInfo, status } = info.row.original\n\t\t\t\tconst { major, minor } = parseSemVer(sysInfo.v)\n\t\t\t\tconst { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: [\"colorWarn\", \"colorCrit\"] })\n\t\t\t\tconst loadAverages = sysInfo.la || []\n\n\t\t\t\tconst max = Math.max(...loadAverages)\n\t\t\t\tif (max === 0 && (status === SystemStatus.Paused || (major < 1 && minor < 13))) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\n\t\t\t\tconst normalizedLoad = max / (sysInfo.t ?? 1)\n\t\t\t\tconst threshold = getMeterStateByThresholds(normalizedLoad * 100, colorWarn, colorCrit)\n\n\t\t\t\treturn (\n\t\t\t\t\t<div className=\"flex items-center gap-[.35em] w-full tabular-nums tracking-tight\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\"inline-block size-2 rounded-full me-0.5\", {\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Up]]: threshold === MeterState.Good,\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Pending]]: threshold === MeterState.Warn,\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Down]]: threshold === MeterState.Crit,\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Paused]]: status !== SystemStatus.Up,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{loadAverages?.map((la, i) => (\n\t\t\t\t\t\t\t<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info, status }) => (status !== SystemStatus.Up ? undefined : info.bb),\n\t\t\tid: \"net\",\n\t\t\tname: () => t`Net`,\n\t\t\tsize: 0,\n\t\t\tIcon: EthernetIcon,\n\t\t\theader: sortableHeader,\n\t\t\tsortUndefined: \"last\",\n\t\t\tcell(info) {\n\t\t\t\tconst val = info.getValue() as number | undefined\n\t\t\t\tif (val === undefined) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst userSettings = useStore($userSettings, { keys: [\"unitNet\"] })\n\t\t\t\tconst { value, unit } = formatBytes(val, true, userSettings.unitNet, false)\n\t\t\t\treturn (\n\t\t\t\t\t<span className=\"tabular-nums whitespace-nowrap\">\n\t\t\t\t\t\t{decimalString(value, value >= 100 ? 1 : 2)} {unit}\n\t\t\t\t\t</span>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.dt,\n\t\t\tid: \"temp\",\n\t\t\tname: () => t({ message: \"Temp\", comment: \"Temperature label in systems table\" }),\n\t\t\tsize: 50,\n\t\t\thideSort: true,\n\t\t\tIcon: ThermometerIcon,\n\t\t\theader: sortableHeader,\n\t\t\tcell(info) {\n\t\t\t\tconst val = info.getValue() as number\n\t\t\t\tconst userSettings = useStore($userSettings, { keys: [\"unitTemp\"] })\n\t\t\t\tif (!val) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst { value, unit } = formatTemperature(val, userSettings.unitTemp)\n\t\t\t\treturn (\n\t\t\t\t\t<span className={cn(\"tabular-nums whitespace-nowrap\", viewMode === \"table\" && \"ps-0.5\")}>\n\t\t\t\t\t\t{decimalString(value, value >= 100 ? 1 : 2)} {unit}\n\t\t\t\t\t</span>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.bat?.[0],\n\t\t\tid: \"battery\",\n\t\t\tname: () => t({ message: \"Bat\", comment: \"Battery label in systems table header\" }),\n\t\t\tsize: 70,\n\t\t\tIcon: BatteryMediumIcon,\n\t\t\theader: sortableHeader,\n\t\t\thideSort: true,\n\t\t\tcell(info) {\n\t\t\t\tconst [pct, state] = info.row.original.info.bat ?? []\n\t\t\t\tif (pct === undefined) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\n\t\t\t\tlet Icon = PlugChargingIcon\n\t\t\t\tlet iconColor = \"text-muted-foreground\"\n\n\t\t\t\tif (state !== BatteryState.Charging) {\n\t\t\t\t\tif (pct < 25) {\n\t\t\t\t\t\ticonColor = pct < 11 ? \"text-red-500\" : \"text-yellow-500\"\n\t\t\t\t\t\tIcon = BatteryLowIcon\n\t\t\t\t\t} else if (pct < 75) {\n\t\t\t\t\t\tIcon = BatteryMediumIcon\n\t\t\t\t\t} else if (pct < 95) {\n\t\t\t\t\t\tIcon = BatteryHighIcon\n\t\t\t\t\t} else {\n\t\t\t\t\t\tIcon = BatteryFullIcon\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst stateLabel =\n\t\t\t\t\tstate !== undefined ? (batteryStateTranslations[state as BatteryState]?.() ?? undefined) : undefined\n\n\t\t\t\treturn (\n\t\t\t\t\t<Link\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\thref={getPagePath($router, \"system\", { id: info.row.original.id })}\n\t\t\t\t\t\tclassName=\"flex items-center gap-1 tabular-nums tracking-tight relative z-10\"\n\t\t\t\t\t\ttitle={stateLabel}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Icon className={cn(\"size-3.5\", iconColor)} />\n\t\t\t\t\t\t<span className=\"min-w-10\">{pct}%</span>\n\t\t\t\t\t</Link>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.sv?.[0],\n\t\t\tid: \"services\",\n\t\t\tname: () => t`Services`,\n\t\t\tsize: 50,\n\t\t\tIcon: TerminalSquareIcon,\n\t\t\theader: sortableHeader,\n\t\t\thideSort: true,\n\t\t\tsortingFn: (a, b) => {\n\t\t\t\t// sort priorities: 1) failed services, 2) total services\n\t\t\t\tconst [totalCountA, numFailedA] = a.original.info.sv ?? [0, 0]\n\t\t\t\tconst [totalCountB, numFailedB] = b.original.info.sv ?? [0, 0]\n\t\t\t\tif (numFailedA !== numFailedB) {\n\t\t\t\t\treturn numFailedA - numFailedB\n\t\t\t\t}\n\t\t\t\treturn totalCountA - totalCountB\n\t\t\t},\n\t\t\tcell(info) {\n\t\t\t\tconst sys = info.row.original\n\t\t\t\tconst [totalCount, numFailed] = sys.info.sv ?? [0, 0]\n\t\t\t\tif (sys.status !== SystemStatus.Up || totalCount === 0) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\treturn (\n\t\t\t\t\t<span className=\"tabular-nums whitespace-nowrap flex gap-1.5 items-center\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\"block size-2 rounded-full\", {\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Down]]: numFailed > 0,\n\t\t\t\t\t\t\t\t[STATUS_COLORS[SystemStatus.Up]]: numFailed === 0,\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{totalCount}{\" \"}\n\t\t\t\t\t\t<span className=\"text-muted-foreground text-sm -ms-0.5\">\n\t\t\t\t\t\t\t({t`Failed`.toLowerCase()}: {numFailed})\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</span>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.u || undefined,\n\t\t\tid: \"uptime\",\n\t\t\tname: () => t`Uptime`,\n\t\t\tsize: 50,\n\t\t\tIcon: ClockArrowUp,\n\t\t\theader: sortableHeader,\n\t\t\thideSort: true,\n\t\t\tcell(info) {\n\t\t\t\tconst uptime = info.getValue() as number\n\t\t\t\tif (!uptime) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\treturn <span className=\"tabular-nums whitespace-nowrap\">{secondsToUptimeString(uptime)}</span>\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\taccessorFn: ({ info }) => info.v,\n\t\t\tid: \"agent\",\n\t\t\tname: () => t`Agent`,\n\t\t\tsize: 50,\n\t\t\tIcon: WifiIcon,\n\t\t\thideSort: true,\n\t\t\theader: sortableHeader,\n\t\t\tcell(info) {\n\t\t\t\tconst version = info.getValue() as string\n\t\t\t\tif (!version) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst system = info.row.original\n\t\t\t\tconst color = {\n\t\t\t\t\t\"text-green-500\": version === globalThis.BESZEL.HUB_VERSION,\n\t\t\t\t\t\"text-yellow-500\": version !== globalThis.BESZEL.HUB_VERSION,\n\t\t\t\t\t\"text-red-500\": system.status !== SystemStatus.Up,\n\t\t\t\t}\n\t\t\t\treturn (\n\t\t\t\t\t<Link\n\t\t\t\t\t\thref={getPagePath($router, \"system\", { id: system.id })}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"flex gap-1.5 items-center md:pe-5 tabular-nums relative z-10\",\n\t\t\t\t\t\t\tviewMode === \"table\" && \"ps-0.5\"\n\t\t\t\t\t\t)}\n\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\ttitle={connectionTypeLabels[system.info.ct as ConnectionType]}\n\t\t\t\t\t\trole=\"none\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{system.info.ct === ConnectionType.WebSocket && (\n\t\t\t\t\t\t\t<WebSocketIcon className={cn(\"size-3 pointer-events-none\", color)} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{system.info.ct === ConnectionType.SSH && (\n\t\t\t\t\t\t\t<ChevronRightSquareIcon className={cn(\"size-3 pointer-events-none\", color)} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{!system.info.ct && <IndicatorDot system={system} className={cn(color, \"bg-current mx-0.5\")} />}\n\t\t\t\t\t\t<span className=\"truncate max-w-14\">{info.getValue() as string}</span>\n\t\t\t\t\t</Link>\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: \"actions\",\n\t\t\t// @ts-expect-error\n\t\t\tname: () => t({ message: \"Actions\", comment: \"Table column\" }),\n\t\t\tsize: 50,\n\t\t\tcell: ({ row }) => (\n\t\t\t\t<div className=\"relative z-10 flex justify-end items-center gap-1 -ms-3\">\n\t\t\t\t\t<AlertButton system={row.original} />\n\t\t\t\t\t<ActionsButton system={row.original} />\n\t\t\t\t</div>\n\t\t\t),\n\t\t},\n\t] as ColumnDef<SystemRecord>[]\n}\n\nfunction sortableHeader(context: HeaderContext<SystemRecord, unknown>) {\n\tconst { column } = context\n\t// @ts-expect-error\n\tconst { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef\n\tconst isSorted = column.getIsSorted()\n\treturn (\n\t\t<Button\n\t\t\tvariant=\"ghost\"\n\t\t\tclassName={cn(\"h-9 px-3 flex duration-50\", isSorted && \"bg-accent/70 light:bg-accent text-accent-foreground/90\")}\n\t\t\tonClick={() => column.toggleSorting(column.getIsSorted() === \"asc\")}\n\t\t>\n\t\t\t{Icon && <Icon className=\"me-2 size-4\" />}\n\t\t\t{name()}\n\t\t\t{hideSort || <ArrowUpDownIcon className=\"ms-2 size-4\" />}\n\t\t</Button>\n\t)\n}\n\nfunction TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {\n\tconst { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: [\"colorWarn\", \"colorCrit\"] })\n\tconst val = Number(info.getValue()) || 0\n\tconst threshold = getMeterStateByThresholds(val, colorWarn, colorCrit)\n\tconst meterClass = cn(\n\t\t\"h-full\",\n\t\t(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||\n\t\t\t(threshold === MeterState.Good && STATUS_COLORS.up) ||\n\t\t\t(threshold === MeterState.Warn && STATUS_COLORS.pending) ||\n\t\t\tSTATUS_COLORS.down\n\t)\n\treturn (\n\t\t<div className=\"flex gap-2 items-center tabular-nums tracking-tight w-full\">\n\t\t\t<span className=\"min-w-8 shrink-0\">{decimalString(val, val >= 10 ? 1 : 2)}%</span>\n\t\t\t<span className=\"flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden\">\n\t\t\t\t<span className={meterClass} style={{ width: `${val}%` }}></span>\n\t\t\t</span>\n\t\t</div>\n\t)\n}\n\nfunction DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {\n\tconst { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: [\"colorWarn\", \"colorCrit\"] })\n\tconst { info: sysInfo, status, id } = info.row.original\n\tconst extraFs = Object.entries(sysInfo.efs ?? {})\n\tconst rootDiskPct = sysInfo.dp\n\n\t// sort extra disks by percentage descending\n\textraFs.sort((a, b) => b[1] - a[1])\n\n\tfunction getIndicatorColor(pct: number) {\n\t\tconst threshold = getMeterStateByThresholds(pct, colorWarn, colorCrit)\n\t\treturn (\n\t\t\t(status !== SystemStatus.Up && STATUS_COLORS.paused) ||\n\t\t\t(threshold === MeterState.Good && STATUS_COLORS.up) ||\n\t\t\t(threshold === MeterState.Warn && STATUS_COLORS.pending) ||\n\t\t\tSTATUS_COLORS.down\n\t\t)\n\t}\n\n\tfunction getMeterClass(pct: number) {\n\t\treturn cn(\"h-full\", getIndicatorColor(pct))\n\t}\n\n\t// Extra disk indicators (max 3 dots - one per state if any disk exists in range)\n\tconst stateColors = [STATUS_COLORS.up, STATUS_COLORS.pending, STATUS_COLORS.down]\n\tconst extraDiskIndicators =\n\t\tstatus !== SystemStatus.Up\n\t\t\t? []\n\t\t\t: [...new Set(extraFs.map(([, pct]) => getMeterStateByThresholds(pct, colorWarn, colorCrit)))]\n\t\t\t\t\t.sort()\n\t\t\t\t\t.map((state) => stateColors[state])\n\n\treturn (\n\t\t<Tooltip>\n\t\t\t<TooltipTrigger asChild>\n\t\t\t\t<Link\n\t\t\t\t\thref={getPagePath($router, \"system\", { id })}\n\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\tclassName=\"flex flex-col gap-0.5 w-full relative z-10\"\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex gap-2 items-center tabular-nums tracking-tight\">\n\t\t\t\t\t\t<span className=\"min-w-8 shrink-0\">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>\n\t\t\t\t\t\t<span className=\"flex-1 min-w-8 flex items-center gap-0.5 px-1 justify-end bg-muted h-[1em] rounded-sm overflow-hidden relative\">\n\t\t\t\t\t\t\t{/* Root disk */}\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\"absolute inset-0\", getMeterClass(rootDiskPct))}\n\t\t\t\t\t\t\t\tstyle={{ width: `${rootDiskPct}%` }}\n\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t{/* Extra disk indicators */}\n\t\t\t\t\t\t\t{extraDiskIndicators.map((color) => (\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tkey={color}\n\t\t\t\t\t\t\t\t\tclassName={cn(\"size-1.5 rounded-full shrink-0 outline-[0.5px] outline-muted\", color)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</Link>\n\t\t\t</TooltipTrigger>\n\t\t\t<TooltipContent side=\"right\" className=\"max-w-xs pb-2\">\n\t\t\t\t<div className=\"grid gap-1\">\n\t\t\t\t\t<div className=\"grid gap-0.5\">\n\t\t\t\t\t\t<div className=\"text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums\">\n\t\t\t\t\t\t\t<Trans context=\"Root disk label\">Root</Trans>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex gap-2 items-center tabular-nums text-xs\">\n\t\t\t\t\t\t\t<span className=\"min-w-7\">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>\n\t\t\t\t\t\t\t<span className=\"flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden\">\n\t\t\t\t\t\t\t\t<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t{extraFs.map(([name, pct]) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div key={name} className=\"grid gap-0.5\">\n\t\t\t\t\t\t\t\t<div className=\"text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate\">\n\t\t\t\t\t\t\t\t\t{name}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"flex gap-2 items-center tabular-nums text-xs\">\n\t\t\t\t\t\t\t\t\t<span className=\"min-w-7\">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>\n\t\t\t\t\t\t\t\t\t<span className=\"flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden\">\n\t\t\t\t\t\t\t\t\t\t<span className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\t\t\t</TooltipContent>\n\t\t</Tooltip>\n\t)\n}\n\nexport function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {\n\tclassName ||= STATUS_COLORS[system.status as keyof typeof STATUS_COLORS] || \"\"\n\treturn (\n\t\t<span\n\t\t\tclassName={cn(\"shrink-0 size-2 rounded-full\", className)}\n\t\t\t// style={{ marginBottom: \"-1px\" }}\n\t\t/>\n\t)\n}\n\nexport const ActionsButton = memo(({ system }: { system: SystemRecord }) => {\n\tconst [deleteOpen, setDeleteOpen] = useState(false)\n\tconst [editOpen, setEditOpen] = useState(false)\n\tconst editOpened = useRef(false)\n\tconst { t } = useLingui()\n\tconst { id, status, host, name } = system\n\n\treturn useMemo(() => {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<DropdownMenu>\n\t\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t\t<Button variant=\"ghost\" size={\"icon\"}>\n\t\t\t\t\t\t\t<span className=\"sr-only\">\n\t\t\t\t\t\t\t\t<Trans>Open menu</Trans>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<MoreHorizontalIcon className=\"w-5\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t\t<DropdownMenuContent align=\"end\">\n\t\t\t\t\t\t{!isReadOnlyUser() && (\n\t\t\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\teditOpened.current = true\n\t\t\t\t\t\t\t\t\tsetEditOpen(true)\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<PenBoxIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t<Trans>Edit</Trans>\n\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\t\tclassName={cn(isReadOnlyUser() && \"hidden\")}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tpb.collection(\"systems\").update(id, {\n\t\t\t\t\t\t\t\t\tstatus: status === SystemStatus.Paused ? SystemStatus.Pending : SystemStatus.Paused,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{status === SystemStatus.Paused ? (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<PlayCircleIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t\t<Trans>Resume</Trans>\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t<PauseCircleIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t\t\t<Trans>Pause</Trans>\n\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t<DropdownMenuItem onClick={() => copyToClipboard(name)}>\n\t\t\t\t\t\t\t<CopyIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t<Trans>Copy name</Trans>\n\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t<DropdownMenuItem onClick={() => copyToClipboard(host)}>\n\t\t\t\t\t\t\t<CopyIcon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t<Trans>Copy host</Trans>\n\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t<DropdownMenuSeparator className={cn(isReadOnlyUser() && \"hidden\")} />\n\t\t\t\t\t\t<DropdownMenuItem className={cn(isReadOnlyUser() && \"hidden\")} onSelect={() => setDeleteOpen(true)}>\n\t\t\t\t\t\t\t<Trash2Icon className=\"me-2.5 size-4\" />\n\t\t\t\t\t\t\t<Trans>Delete</Trans>\n\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t</DropdownMenuContent>\n\t\t\t\t</DropdownMenu>\n\t\t\t\t{/* edit dialog */}\n\t\t\t\t<Dialog open={editOpen} onOpenChange={setEditOpen}>\n\t\t\t\t\t{editOpened.current && <SystemDialog system={system} setOpen={setEditOpen} />}\n\t\t\t\t</Dialog>\n\t\t\t\t{/* deletion dialog */}\n\t\t\t\t<AlertDialog open={deleteOpen} onOpenChange={(open) => setDeleteOpen(open)}>\n\t\t\t\t\t<AlertDialogContent>\n\t\t\t\t\t\t<AlertDialogHeader>\n\t\t\t\t\t\t\t<AlertDialogTitle>\n\t\t\t\t\t\t\t\t<Trans>Are you sure you want to delete {name}?</Trans>\n\t\t\t\t\t\t\t</AlertDialogTitle>\n\t\t\t\t\t\t\t<AlertDialogDescription>\n\t\t\t\t\t\t\t\t<Trans>\n\t\t\t\t\t\t\t\t\tThis action cannot be undone. This will permanently delete all current records for {name} from the\n\t\t\t\t\t\t\t\t\tdatabase.\n\t\t\t\t\t\t\t\t</Trans>\n\t\t\t\t\t\t\t</AlertDialogDescription>\n\t\t\t\t\t\t</AlertDialogHeader>\n\t\t\t\t\t\t<AlertDialogFooter>\n\t\t\t\t\t\t\t<AlertDialogCancel>\n\t\t\t\t\t\t\t\t<Trans>Cancel</Trans>\n\t\t\t\t\t\t\t</AlertDialogCancel>\n\t\t\t\t\t\t\t<AlertDialogAction\n\t\t\t\t\t\t\t\tclassName={cn(buttonVariants({ variant: \"destructive\" }))}\n\t\t\t\t\t\t\t\tonClick={() => pb.collection(\"systems\").delete(id)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Trans>Continue</Trans>\n\t\t\t\t\t\t\t</AlertDialogAction>\n\t\t\t\t\t\t</AlertDialogFooter>\n\t\t\t\t\t</AlertDialogContent>\n\t\t\t\t</AlertDialog>\n\t\t\t</>\n\t\t)\n\t}, [id, status, host, name, system, t, deleteOpen, editOpen])\n})\n"
  },
  {
    "path": "internal/site/src/components/systems-table/systems-table.tsx",
    "content": "import { Trans, useLingui } from \"@lingui/react/macro\"\nimport { useStore } from \"@nanostores/react\"\nimport { getPagePath } from \"@nanostores/router\"\nimport {\n\ttype ColumnDef,\n\ttype ColumnFiltersState,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetFilteredRowModel,\n\tgetSortedRowModel,\n\ttype Row,\n\ttype SortingState,\n\ttype Table as TableType,\n\tuseReactTable,\n\ttype VisibilityState,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer, type VirtualItem } from \"@tanstack/react-virtual\"\nimport {\n\tArrowDownIcon,\n\tArrowUpDownIcon,\n\tArrowUpIcon,\n\tEyeIcon,\n\tFilterIcon,\n\tLayoutGridIcon,\n\tLayoutListIcon,\n\tSettings2Icon,\n\tXIcon,\n} from \"lucide-react\"\nimport { memo, useEffect, useMemo, useRef, useState } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n\tDropdownMenu,\n\tDropdownMenuCheckboxItem,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuLabel,\n\tDropdownMenuRadioGroup,\n\tDropdownMenuRadioItem,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport { TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/ui/table\"\nimport { SystemStatus } from \"@/lib/enums\"\nimport { $downSystems, $pausedSystems, $systems, $upSystems } from \"@/lib/stores\"\nimport { cn, runOnce, useBrowserStorage } from \"@/lib/utils\"\nimport type { SystemRecord } from \"@/types\"\nimport AlertButton from \"../alerts/alert-button\"\nimport { $router, Link } from \"../router\"\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"../ui/card\"\nimport { SystemsTableColumns, ActionsButton, IndicatorDot } from \"./systems-table-columns\"\n\ntype ViewMode = \"table\" | \"grid\"\ntype StatusFilter = \"all\" | SystemRecord[\"status\"]\n\nconst preloadSystemDetail = runOnce(() => import(\"@/components/routes/system.tsx\"))\n\nexport default function SystemsTable() {\n\tconst data = useStore($systems)\n\tconst downSystems = $downSystems.get()\n\tconst upSystems = $upSystems.get()\n\tconst pausedSystems = $pausedSystems.get()\n\tconst { i18n, t } = useLingui()\n\tconst [filter, setFilter] = useState<string>(\"\")\n\tconst [statusFilter, setStatusFilter] = useState<StatusFilter>(\"all\")\n\tconst [sorting, setSorting] = useBrowserStorage<SortingState>(\n\t\t\"sortMode\",\n\t\t[{ id: \"system\", desc: false }],\n\t\tsessionStorage\n\t)\n\tconst [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n\tconst [columnVisibility, setColumnVisibility] = useBrowserStorage<VisibilityState>(\"cols\", {})\n\n\tconst locale = i18n.locale\n\n\t// Filter data based on status filter\n\tconst filteredData = useMemo(() => {\n\t\tif (statusFilter === \"all\") {\n\t\t\treturn data\n\t\t}\n\t\tif (statusFilter === SystemStatus.Up) {\n\t\t\treturn Object.values(upSystems) ?? []\n\t\t}\n\t\tif (statusFilter === SystemStatus.Down) {\n\t\t\treturn Object.values(downSystems) ?? []\n\t\t}\n\t\treturn Object.values(pausedSystems) ?? []\n\t}, [data, statusFilter])\n\n\tconst [viewMode, setViewMode] = useBrowserStorage<ViewMode>(\n\t\t\"viewMode\",\n\t\t// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)\n\t\twindow.innerWidth < 1024 && filteredData.length < 200 ? \"grid\" : \"table\"\n\t)\n\n\tuseEffect(() => {\n\t\tif (filter !== undefined) {\n\t\t\ttable.getColumn(\"system\")?.setFilterValue(filter)\n\t\t}\n\t}, [filter])\n\n\tconst columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode])\n\n\tconst table = useReactTable({\n\t\tdata: filteredData,\n\t\tcolumns: columnDefs,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tonSortingChange: setSorting,\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tonColumnVisibilityChange: setColumnVisibility,\n\t\tstate: {\n\t\t\tsorting,\n\t\t\tcolumnFilters,\n\t\t\tcolumnVisibility,\n\t\t},\n\t\tdefaultColumn: {\n\t\t\tinvertSorting: true,\n\t\t\tsortUndefined: \"last\",\n\t\t\tminSize: 0,\n\t\t\tsize: 900,\n\t\t\tmaxSize: 900,\n\t\t},\n\t})\n\n\tconst rows = table.getRowModel().rows\n\tconst columns = table.getAllColumns()\n\tconst visibleColumns = table.getVisibleLeafColumns()\n\n\tconst [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => {\n\t\treturn [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length]\n\t}, [upSystems, downSystems, pausedSystems])\n\n\tconst CardHead = useMemo(() => {\n\t\treturn (\n\t\t\t<CardHeader className=\"pb-4.5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1\">\n\t\t\t\t<div className=\"grid md:flex gap-5 w-full items-end\">\n\t\t\t\t\t<div className=\"px-2 sm:px-1\">\n\t\t\t\t\t\t<CardTitle className=\"mb-2\">\n\t\t\t\t\t\t\t<Trans>All Systems</Trans>\n\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t<CardDescription className=\"flex\">\n\t\t\t\t\t\t\t<Trans>Click on a system to view more information.</Trans>\n\t\t\t\t\t\t</CardDescription>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"flex gap-2 ms-auto w-full md:w-80\">\n\t\t\t\t\t\t<div className=\"relative flex-1\">\n\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\tplaceholder={t`Filter...`}\n\t\t\t\t\t\t\t\tonChange={(e) => setFilter(e.target.value)}\n\t\t\t\t\t\t\t\tvalue={filter}\n\t\t\t\t\t\t\t\tclassName=\"ps-4 pe-10 w-full\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t{filter && (\n\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\taria-label={t`Clear`}\n\t\t\t\t\t\t\t\t\tclassName=\"absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 text-muted-foreground\"\n\t\t\t\t\t\t\t\t\tonClick={() => setFilter(\"\")}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<XIcon className=\"h-4 w-4\" />\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<DropdownMenu>\n\t\t\t\t\t\t\t<DropdownMenuTrigger asChild>\n\t\t\t\t\t\t\t\t<Button variant=\"outline\">\n\t\t\t\t\t\t\t\t\t<Settings2Icon className=\"me-1.5 size-4 opacity-80\" />\n\t\t\t\t\t\t\t\t\t<Trans>View</Trans>\n\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t</DropdownMenuTrigger>\n\t\t\t\t\t\t\t<DropdownMenuContent align=\"end\" className=\"h-72 md:h-auto min-w-48 md:min-w-auto overflow-y-auto\">\n\t\t\t\t\t\t\t\t<div className=\"grid grid-cols-1 md:grid-cols-4 divide-y md:divide-s md:divide-y-0\">\n\t\t\t\t\t\t\t\t\t<div className=\"border-r\">\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuLabel className=\"pt-2 px-3.5 flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t<LayoutGridIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Layout</Trans>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuLabel>\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioGroup\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"px-1 pb-1\"\n\t\t\t\t\t\t\t\t\t\t\tvalue={viewMode}\n\t\t\t\t\t\t\t\t\t\t\tonValueChange={(view) => setViewMode(view as ViewMode)}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"table\" onSelect={(e) => e.preventDefault()} className=\"gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<LayoutListIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Table</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"grid\" onSelect={(e) => e.preventDefault()} className=\"gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t\t<LayoutGridIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Grid</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioGroup>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div className=\"border-r\">\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuLabel className=\"pt-2 px-3.5 flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t<FilterIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Status</Trans>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuLabel>\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioGroup\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"px-1 pb-1\"\n\t\t\t\t\t\t\t\t\t\t\tvalue={statusFilter}\n\t\t\t\t\t\t\t\t\t\t\tonValueChange={(value) => setStatusFilter(value as StatusFilter)}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"all\" onSelect={(e) => e.preventDefault()}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>All Systems</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"up\" onSelect={(e) => e.preventDefault()}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Up ({upSystemsLength})</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"down\" onSelect={(e) => e.preventDefault()}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Down ({downSystemsLength})</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuRadioItem value=\"paused\" onSelect={(e) => e.preventDefault()}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Trans>Paused ({pausedSystemsLength})</Trans>\n\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioItem>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuRadioGroup>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div className=\"border-r\">\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuLabel className=\"pt-2 px-3.5 flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t<ArrowUpDownIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Sort By</Trans>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuLabel>\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<div className=\"px-1 pb-1\">\n\t\t\t\t\t\t\t\t\t\t\t{columns.map((column) => {\n\t\t\t\t\t\t\t\t\t\t\t\tif (!column.getCanSort()) return null\n\t\t\t\t\t\t\t\t\t\t\t\tlet Icon = <span className=\"w-6\"></span>\n\t\t\t\t\t\t\t\t\t\t\t\t// if current sort column, show sort direction\n\t\t\t\t\t\t\t\t\t\t\t\tif (sorting[0]?.id === column.id) {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (sorting[0]?.desc) {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tIcon = <ArrowUpIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tIcon = <ArrowDownIcon className=\"me-2 size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuItem\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonSelect={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetSorting([{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }])\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={column.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{Icon}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{column.columnDef.name()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuItem>\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuLabel className=\"pt-2 px-3.5 flex items-center gap-2\">\n\t\t\t\t\t\t\t\t\t\t\t<EyeIcon className=\"size-4\" />\n\t\t\t\t\t\t\t\t\t\t\t<Trans>Visible Fields</Trans>\n\t\t\t\t\t\t\t\t\t\t</DropdownMenuLabel>\n\t\t\t\t\t\t\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t\t\t\t\t\t\t<div className=\"px-1.5 pb-1\">\n\t\t\t\t\t\t\t\t\t\t\t{columns\n\t\t\t\t\t\t\t\t\t\t\t\t.filter((column) => column.getCanHide())\n\t\t\t\t\t\t\t\t\t\t\t\t.map((column) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<DropdownMenuCheckboxItem\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={column.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonSelect={(e) => e.preventDefault()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={column.getIsVisible()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonCheckedChange={(value) => column.toggleVisibility(!!value)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{column.columnDef.name()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</DropdownMenuCheckboxItem>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</DropdownMenuContent>\n\t\t\t\t\t\t</DropdownMenu>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</CardHeader>\n\t\t)\n\t}, [\n\t\tvisibleColumns.length,\n\t\tsorting,\n\t\tviewMode,\n\t\tlocale,\n\t\tstatusFilter,\n\t\tupSystemsLength,\n\t\tdownSystemsLength,\n\t\tpausedSystemsLength,\n\t\tfilter,\n\t])\n\n\treturn (\n\t\t<Card>\n\t\t\t{CardHead}\n\t\t\t<div className=\"p-6 pt-0 max-sm:py-3 max-sm:px-2\">\n\t\t\t\t{viewMode === \"table\" ? (\n\t\t\t\t\t// table layout\n\t\t\t\t\t<div className=\"rounded-md\">\n\t\t\t\t\t\t<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />\n\t\t\t\t\t</div>\n\t\t\t\t) : (\n\t\t\t\t\t// grid layout\n\t\t\t\t\t<div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n\t\t\t\t\t\t{rows?.length ? (\n\t\t\t\t\t\t\trows.map((row) => {\n\t\t\t\t\t\t\t\treturn <SystemCard key={row.original.id} row={row} table={table} colLength={visibleColumns.length} />\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<div className=\"col-span-full text-center py-8\">\n\t\t\t\t\t\t\t\t<Trans>No systems found.</Trans>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</Card>\n\t)\n}\n\nconst AllSystemsTable = memo(\n\t({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {\n\t\t// The virtualizer will need a reference to the scrollable container element\n\t\tconst scrollRef = useRef<HTMLDivElement>(null)\n\n\t\tconst virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n\t\t\tcount: rows.length,\n\t\t\testimateSize: () => (rows.length > 10 ? 56 : 60),\n\t\t\tgetScrollElement: () => scrollRef.current,\n\t\t\toverscan: 5,\n\t\t})\n\t\tconst virtualRows = virtualizer.getVirtualItems()\n\n\t\tconst paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)\n\t\tconst paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md\",\n\t\t\t\t\t// don't set min height if there are less than 2 rows, do set if we need to display the empty state\n\t\t\t\t\t(!rows.length || rows.length > 2) && \"min-h-50\"\n\t\t\t\t)}\n\t\t\t\tref={scrollRef}\n\t\t\t>\n\t\t\t\t{/* add header height to table size */}\n\t\t\t\t<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>\n\t\t\t\t\t<table className=\"text-sm w-full h-full\">\n\t\t\t\t\t\t<SystemsTableHead table={table} />\n\t\t\t\t\t\t<TableBody onMouseEnter={preloadSystemDetail}>\n\t\t\t\t\t\t\t{rows.length ? (\n\t\t\t\t\t\t\t\tvirtualRows.map((virtualRow) => {\n\t\t\t\t\t\t\t\t\tconst row = rows[virtualRow.index] as Row<SystemRecord>\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<SystemTableRow\n\t\t\t\t\t\t\t\t\t\t\tkey={row.id}\n\t\t\t\t\t\t\t\t\t\t\trow={row}\n\t\t\t\t\t\t\t\t\t\t\tvirtualRow={virtualRow}\n\t\t\t\t\t\t\t\t\t\t\tlength={rows.length}\n\t\t\t\t\t\t\t\t\t\t\tcolLength={colLength}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t\t<TableCell colSpan={colLength} className=\"h-37 text-center pointer-events-none\">\n\t\t\t\t\t\t\t\t\t\t<Trans>No systems found.</Trans>\n\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</TableBody>\n\t\t\t\t\t</table>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t)\n\t}\n)\n\nfunction SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {\n\tconst { t } = useLingui()\n\treturn (\n\t\t<TableHeader className=\"sticky top-0 z-50 w-full border-b-2\">\n\t\t\t<div className=\"absolute -top-2 left-0 w-full h-4 bg-table-header z-50\"></div>\n\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t<tr key={headerGroup.id}>\n\t\t\t\t\t{headerGroup.headers.map((header) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<TableHead className=\"px-1.5\" key={header.id}>\n\t\t\t\t\t\t\t\t{flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t</tr>\n\t\t\t))}\n\t\t</TableHeader>\n\t)\n}\n\nconst SystemTableRow = memo(\n\t({\n\t\trow,\n\t\tvirtualRow,\n\t\tcolLength,\n\t}: {\n\t\trow: Row<SystemRecord>\n\t\tvirtualRow: VirtualItem\n\t\tlength: number\n\t\tcolLength: number\n\t}) => {\n\t\tconst system = row.original\n\t\tconst { t } = useLingui()\n\t\treturn useMemo(() => {\n\t\t\treturn (\n\t\t\t\t<TableRow\n\t\t\t\t\t// data-state={row.getIsSelected() && \"selected\"}\n\t\t\t\t\tclassName={cn(\"cursor-pointer transition-opacity relative safari:transform-3d\", {\n\t\t\t\t\t\t\"opacity-50\": system.status === SystemStatus.Paused,\n\t\t\t\t\t})}\n\t\t\t\t>\n\t\t\t\t\t{row.getVisibleCells().map((cell) => (\n\t\t\t\t\t\t<TableCell\n\t\t\t\t\t\t\tkey={cell.id}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: cell.column.getSize(),\n\t\t\t\t\t\t\t\theight: virtualRow.size,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"py-0 ps-4.5\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t))}\n\t\t\t\t</TableRow>\n\t\t\t)\n\t\t}, [system, system.status, colLength, t])\n\t}\n)\n\nconst SystemCard = memo(\n\t({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {\n\t\tconst system = row.original\n\t\tconst { t } = useLingui()\n\n\t\treturn useMemo(() => {\n\t\t\treturn (\n\t\t\t\t<Card\n\t\t\t\t\tonMouseEnter={preloadSystemDetail}\n\t\t\t\t\tkey={system.id}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"opacity-50\": system.status === SystemStatus.Paused,\n\t\t\t\t\t\t}\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<CardHeader className=\"py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60\">\n\t\t\t\t\t\t<div className=\"flex items-center gap-2 w-full overflow-hidden\">\n\t\t\t\t\t\t\t<CardTitle className=\"text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5\">\n\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-2.5 min-w-0 flex-1\">\n\t\t\t\t\t\t\t\t\t<IndicatorDot system={system} />\n\t\t\t\t\t\t\t\t\t<span className=\"text-[.95em]/normal tracking-normal text-primary/90 truncate\">{system.name}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</CardTitle>\n\t\t\t\t\t\t\t{table.getColumn(\"actions\")?.getIsVisible() && (\n\t\t\t\t\t\t\t\t<div className=\"flex gap-1 shrink-0 relative z-10\">\n\t\t\t\t\t\t\t\t\t<AlertButton system={system} />\n\t\t\t\t\t\t\t\t\t<ActionsButton system={system} />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</CardHeader>\n\t\t\t\t\t<CardContent className=\"text-sm px-5 pt-3.5 pb-4\">\n\t\t\t\t\t\t<div className=\"grid gap-2.5\" style={{ gridTemplateColumns: \"24px minmax(80px, max-content) 1fr\" }}>\n\t\t\t\t\t\t\t{table.getAllColumns().map((column) => {\n\t\t\t\t\t\t\t\tif (!column.getIsVisible() || column.id === \"system\" || column.id === \"actions\") return null\n\t\t\t\t\t\t\t\tconst cell = row.getAllCells().find((cell) => cell.column.id === column.id)\n\t\t\t\t\t\t\t\tif (!cell) return null\n\t\t\t\t\t\t\t\t// @ts-expect-error\n\t\t\t\t\t\t\t\tconst { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t<div key={`${column.id}-icon`} className=\"flex items-center\">\n\t\t\t\t\t\t\t\t\t\t\t{column.id === \"lastSeen\" ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<EyeIcon className=\"size-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\tIcon && <Icon className=\"size-4 text-muted-foreground\" />\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div key={`${column.id}-label`} className=\"flex items-center text-muted-foreground pr-3\">\n\t\t\t\t\t\t\t\t\t\t\t{name()}:\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div key={`${column.id}-value`} className=\"flex items-center\">\n\t\t\t\t\t\t\t\t\t\t\t{flexRender(cell.column.columnDef.cell, cell.getContext())}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</CardContent>\n\t\t\t\t\t<Link\n\t\t\t\t\t\thref={getPagePath($router, \"system\", { id: row.original.id })}\n\t\t\t\t\t\tclassName=\"inset-0 absolute w-full h-full\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"sr-only\">{row.original.name}</span>\n\t\t\t\t\t</Link>\n\t\t\t\t</Card>\n\t\t\t)\n\t\t}, [system, colLength, t])\n\t}\n)\n"
  },
  {
    "path": "internal/site/src/components/theme-provider.tsx",
    "content": "import { createContext, useContext, useEffect, useState } from \"react\"\n\ntype Theme = \"dark\" | \"light\" | \"system\"\n\ntype ThemeProviderProps = {\n\tchildren: React.ReactNode\n\tdefaultTheme?: Theme\n\tstorageKey?: string\n}\n\ntype ThemeProviderState = {\n\ttheme: Theme\n\tsetTheme: (theme: Theme) => void\n}\n\nconst initialState: ThemeProviderState = {\n\ttheme: \"system\",\n\tsetTheme: () => null,\n}\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState)\n\nexport function ThemeProvider({\n\tchildren,\n\tdefaultTheme = \"system\",\n\tstorageKey = \"ui-theme\",\n\t...props\n}: ThemeProviderProps) {\n\tconst [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme)\n\n\tuseEffect(() => {\n\t\tconst root = window.document.documentElement\n\n\t\troot.classList.remove(\"light\", \"dark\")\n\n\t\tif (theme === \"system\") {\n\t\t\tconst systemTheme = window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\"\n\n\t\t\troot.classList.add(systemTheme)\n\t\t\treturn\n\t\t}\n\n\t\troot.classList.add(theme)\n\t}, [theme])\n\n\tconst value = {\n\t\ttheme,\n\t\tsetTheme: (theme: Theme) => {\n\t\t\tlocalStorage.setItem(storageKey, theme)\n\t\t\tsetTheme(theme)\n\t\t},\n\t}\n\n\treturn (\n\t\t<ThemeProviderContext.Provider {...props} value={value}>\n\t\t\t{children}\n\t\t</ThemeProviderContext.Provider>\n\t)\n}\n\nexport const useTheme = () => useContext(ThemeProviderContext)\n"
  },
  {
    "path": "internal/site/src/components/ui/alert-dialog.tsx",
    "content": "import * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\nimport * as React from \"react\"\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\nconst AlertDialog = AlertDialogPrimitive.Root\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal\n\nconst AlertDialogOverlay = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPrimitive.Overlay\n\t\tclassName={cn(\n\t\t\t\"fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t\tref={ref}\n\t/>\n))\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName\n\nconst AlertDialogContent = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Content>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPortal>\n\t\t<AlertDialogOverlay />\n\t\t<AlertDialogPrimitive.Content\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t</AlertDialogPortal>\n))\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n\t<div className={cn(\"grid gap-2 text-center sm:text-start\", className)} {...props} />\n)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n\t<div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2\", className)} {...props} />\n)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Title>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPrimitive.Title ref={ref} className={cn(\"text-lg font-semibold\", className)} {...props} />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Description>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n))\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Action>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n\tReact.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n\tReact.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n\t<AlertDialogPrimitive.Cancel\n\t\tref={ref}\n\t\tclassName={cn(buttonVariants({ variant: \"outline\" }), \"mt-2 sm:mt-0\", className)}\n\t\t{...props}\n\t/>\n))\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName\n\nexport {\n\tAlertDialog,\n\tAlertDialogPortal,\n\tAlertDialogOverlay,\n\tAlertDialogTrigger,\n\tAlertDialogContent,\n\tAlertDialogHeader,\n\tAlertDialogFooter,\n\tAlertDialogTitle,\n\tAlertDialogDescription,\n\tAlertDialogAction,\n\tAlertDialogCancel,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/alert.tsx",
    "content": "import * as React from \"react\"\n// import { cva, type VariantProps } from 'class-variance-authority'\n\nimport { cn } from \"@/lib/utils\"\n\n// const alertVariants = cva(\n//   \"relative w-full rounded-lg border p-4 [&>svg~*]:ps-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n//   {\n//     variants: {\n//       variant: {\n//         default: \"bg-background text-foreground\",\n//         destructive:\n//           \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n//       },\n//     },\n//     defaultVariants: {\n//       variant: \"default\",\n//     },\n//   }\n// )\n\nconst Alert = React.forwardRef<\n\tHTMLDivElement,\n\t// React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n\t// >(({ className, variant, ...props }, ref) => (\n\tReact.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n\t<div\n\t\tref={ref}\n\t\trole=\"alert\"\n\t\tclassName={cn(\n\t\t\t\"relative w-full rounded-lg border p-4 [&>svg~*]:ps-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground bg-background text-foreground\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<h5 ref={ref} className={cn(\"mb-1 -mt-0.5 font-medium leading-tight tracking-tight\", className)} {...props} />\n\t)\n)\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<div ref={ref} className={cn(\"text-sm [&_p]:leading-relaxed\", className)} {...props} />\n\t)\n)\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n"
  },
  {
    "path": "internal/site/src/components/ui/badge.tsx",
    "content": "import { cva, type VariantProps } from \"class-variance-authority\"\nimport type * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n\t\"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n\t\t\t\tsecondary: \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n\t\t\t\tdestructive: \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n\t\t\t\toutline: \"text-foreground\",\n\t\t\t\tsuccess: \"border-transparent bg-green-200 text-green-800\",\n\t\t\t\tdanger: \"border-transparent bg-red-200 text-red-800\",\n\t\t\t\twarning: \"border-transparent bg-yellow-200 text-yellow-800\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t},\n\t}\n)\n\nexport interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> { }\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n\treturn <div className={cn(badgeVariants({ variant }), className)} {...props} />\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "internal/site/src/components/ui/button.tsx",
    "content": "import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n\t\"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n\t\t\t\tdestructive: \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n\t\t\t\toutline: \"border bg-background hover:bg-accent/70 dark:hover:bg-accent/50 hover:text-accent-foreground\",\n\t\t\t\tsecondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n\t\t\t\tghost: \"hover:bg-accent/70 hover:text-accent-foreground\",\n\t\t\t\tlink: \"text-primary underline-offset-4 hover:underline\",\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: \"h-10 px-4 py-2\",\n\t\t\t\tsm: \"h-9 rounded-md px-3\",\n\t\t\t\tlg: \"h-11 rounded-md px-8\",\n\t\t\t\ticon: \"h-10 w-10\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t\tsize: \"default\",\n\t\t},\n\t}\n)\n\nexport interface ButtonProps\n\textends React.ButtonHTMLAttributes<HTMLButtonElement>,\n\t\tVariantProps<typeof buttonVariants> {\n\tasChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n\t({ className, variant, size, asChild = false, ...props }, ref) => {\n\t\tconst Comp = asChild ? Slot : \"button\"\n\t\treturn <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n\t}\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "internal/site/src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (\n\t<div\n\t\tref={ref}\n\t\tclassName={cn(\"rounded-lg border border-border/60 bg-card text-card-foreground shadow-xs\", className)}\n\t\t{...props}\n\t/>\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n\t({ className, ...props }, ref) => <div ref={ref} className={cn(\"grid gap-1.5 p-6\", className)} {...props} />\n)\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<h3 ref={ref} className={cn(\"text-2xl font-semibold leading-none tracking-tight\", className)} {...props} />\n\t)\n)\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<p ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n\t)\n)\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n\t({ className, ...props }, ref) => <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n)\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n\t({ className, ...props }, ref) => <div ref={ref} className={cn(\"flex items-center p-6 pt-0\", className)} {...props} />\n)\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "internal/site/src/components/ui/chart.tsx",
    "content": "import type { JSX } from \"react\"\nimport { useLingui } from \"@lingui/react/macro\"\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\nimport { chartTimeData, cn } from \"@/lib/utils\"\nimport type { ChartData } from \"@/types\"\nimport { Separator } from \"./separator\"\nimport { AxisDomain } from \"recharts/types/util/types\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nexport type ChartConfig = {\n\t[k in string]: {\n\t\tlabel?: React.ReactNode\n\t\ticon?: React.ComponentType\n\t} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })\n}\n\n// type ChartContextProps = {\n// \tconfig: ChartConfig\n// }\n\n// const ChartContext = React.createContext<ChartContextProps | null>(null)\n\n// function useChart() {\n// \tconst context = React.useContext(ChartContext)\n\n// \tif (!context) {\n// \t\tthrow new Error('useChart must be used within a <ChartContainer />')\n// \t}\n\n// \treturn context\n// }\n\nconst ChartContainer = React.forwardRef<\n\tHTMLDivElement,\n\tReact.ComponentProps<\"div\"> & {\n\t\t// config: ChartConfig\n\t\tchildren: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>[\"children\"]\n\t}\n>(({ id, className, children, ...props }, ref) => {\n\tconst uniqueId = React.useId()\n\tconst chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n\treturn (\n\t\t//<ChartContext.Provider value={{ config }}>\n\t\t//</ChartContext.Provider>\n\t\t<div\n\t\t\tdata-chart={chartId}\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{/* <ChartStyle id={chartId} config={config} /> */}\n\t\t\t<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>\n\t\t</div>\n\t)\n})\nChartContainer.displayName = \"Chart\"\n\n// const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n// \tconst colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)\n\n// \tif (!colorConfig.length) {\n// \t\treturn null\n// \t}\n\n// \treturn (\n// \t\t<style\n// \t\t\tdangerouslySetInnerHTML={{\n// \t\t\t\t__html: Object.entries(THEMES).map(\n// \t\t\t\t\t([theme, prefix]) => `\n// ${prefix} [data-chart=${id}] {\n// ${colorConfig\n// \t.map(([key, itemConfig]) => {\n// \t\tconst color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color\n// \t\treturn color ? `  --color-${key}: ${color};` : null\n// \t})\n// \t.join('\\n')}\n// }\n// `\n// \t\t\t\t),\n// \t\t\t}}\n// \t\t/>\n// \t)\n// }\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nconst ChartTooltipContent = React.forwardRef<\n\tHTMLDivElement,\n\tReact.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n\t\tReact.ComponentProps<\"div\"> & {\n\t\t\thideLabel?: boolean\n\t\t\tindicator?: \"line\" | \"dot\" | \"dashed\"\n\t\t\tnameKey?: string\n\t\t\tlabelKey?: string\n\t\t\tunit?: string\n\t\t\tfilter?: string\n\t\t\tcontentFormatter?: (item: any, key: string) => React.ReactNode | string\n\t\t\ttruncate?: boolean\n\t\t\tshowTotal?: boolean\n\t\t\ttotalLabel?: React.ReactNode\n\t\t}\n>(\n\t(\n\t\t{\n\t\t\tactive,\n\t\t\tpayload,\n\t\t\tclassName,\n\t\t\tindicator = \"line\",\n\t\t\thideLabel = false,\n\t\t\tlabel,\n\t\t\tlabelFormatter,\n\t\t\tlabelClassName,\n\t\t\tformatter,\n\t\t\tcolor,\n\t\t\tnameKey,\n\t\t\tlabelKey,\n\t\t\tunit,\n\t\t\tfilter,\n\t\t\titemSorter,\n\t\t\tcontentFormatter: content = undefined,\n\t\t\ttruncate = false,\n\t\t\tshowTotal = false,\n\t\t\ttotalLabel,\n\t\t},\n\t\tref\n\t) => {\n\t\t// const { config } = useChart()\n\t\tconst config = {}\n\t\tconst { t } = useLingui()\n\t\tconst totalLabelNode = totalLabel ?? t`Total`\n\t\tconst totalName = typeof totalLabelNode === \"string\" ? totalLabelNode : t`Total`\n\n\t\tReact.useMemo(() => {\n\t\t\tif (filter) {\n\t\t\t\tconst filterTerms = filter\n\t\t\t\t\t.toLowerCase()\n\t\t\t\t\t.split(\" \")\n\t\t\t\t\t.filter((term) => term.length > 0)\n\t\t\t\tpayload = payload?.filter((item) => {\n\t\t\t\t\tconst itemName = (item.name as string)?.toLowerCase()\n\t\t\t\t\treturn filterTerms.some((term) => itemName?.includes(term))\n\t\t\t\t})\n\t\t\t}\n\t\t\tif (itemSorter) {\n\t\t\t\t// @ts-expect-error\n\t\t\t\tpayload?.sort(itemSorter)\n\t\t\t}\n\t\t}, [itemSorter, payload])\n\n\t\tconst totalValueDisplay = React.useMemo(() => {\n\t\t\tif (!showTotal || !payload?.length) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tlet totalValue = 0\n\t\t\tlet hasNumericValue = false\n\n\t\t\tfor (const item of payload) {\n\t\t\t\tconst numericValue = typeof item.value === \"number\" ? item.value : Number(item.value)\n\t\t\t\tif (Number.isFinite(numericValue)) {\n\t\t\t\t\ttotalValue += numericValue\n\t\t\t\t\thasNumericValue = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasNumericValue) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst totalKey = \"__total__\"\n\t\t\tconst totalItem: any = {\n\t\t\t\tvalue: totalValue,\n\t\t\t\tname: totalName,\n\t\t\t\tdataKey: totalKey,\n\t\t\t\tcolor,\n\t\t\t}\n\n\t\t\tif (content) {\n\t\t\t\ttotalItem.payload = payload[0]?.payload\n\t\t\t}\n\n\t\t\tif (typeof formatter === \"function\") {\n\t\t\t\treturn formatter(totalValue, totalName, totalItem, payload.length, totalItem.payload ?? payload[0]?.payload)\n\t\t\t}\n\n\t\t\tif (content) {\n\t\t\t\treturn content(totalItem, totalKey)\n\t\t\t}\n\n\t\t\treturn `${totalValue.toLocaleString()}${unit ?? \"\"}`\n\t\t}, [color, content, formatter, nameKey, payload, showTotal, totalName, unit])\n\n\t\tconst tooltipLabel = React.useMemo(() => {\n\t\t\tif (hideLabel || !payload?.length) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst [item] = payload\n\t\t\tconst key = `${labelKey || item.name || \"value\"}`\n\t\t\tconst itemConfig = getPayloadConfigFromPayload(config, item, key)\n\t\t\tconst value = !labelKey && typeof label === \"string\" ? label : itemConfig?.label\n\n\t\t\tif (labelFormatter) {\n\t\t\t\treturn <div className={cn(\"font-medium\", labelClassName)}>{labelFormatter(value, payload)}</div>\n\t\t\t}\n\n\t\t\tif (!value) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\treturn <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n\t\t}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])\n\n\t\tif (!active || !payload?.length) {\n\t\t\treturn null\n\t\t}\n\n\t\t// const nestLabel = payload.length === 1 && indicator !== 'dot'\n\t\tconst nestLabel = false\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"grid min-w-28 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{!nestLabel ? tooltipLabel : null}\n\t\t\t\t<div className=\"grid gap-1.5\">\n\t\t\t\t\t{payload.map((item, index) => {\n\t\t\t\t\t\tconst key = `${nameKey || item.name || item.dataKey || \"value\"}`\n\t\t\t\t\t\tconst itemConfig = getPayloadConfigFromPayload(config, item, key)\n\t\t\t\t\t\tconst indicatorColor = color || item.payload.fill || item.color\n\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={item?.name || item.dataKey}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n\t\t\t\t\t\t\t\t\tindicator === \"dot\" && \"items-center\"\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{formatter && item?.value !== undefined && item.name ? (\n\t\t\t\t\t\t\t\t\tformatter(item.value, item.name, item, index, item.payload)\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t{itemConfig?.icon ? (\n\t\t\t\t\t\t\t\t\t\t\t<itemConfig.icon />\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\"shrink-0 rounded-[2px] border-border bg-(--color-bg)\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"h-2.5 w-2.5\": indicator === \"dot\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"w-1\": indicator === \"line\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"w-0 border-[1.5px] border-dashed bg-transparent\": indicator === \"dashed\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"my-0.5\": nestLabel && indicator === \"dashed\",\n\t\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"--color-bg\": indicatorColor,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"--color-border\": indicatorColor,\n\t\t\t\t\t\t\t\t\t\t\t\t\t} as React.CSSProperties\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\"flex flex-1 justify-between leading-none gap-2\",\n\t\t\t\t\t\t\t\t\t\t\t\tnestLabel ? \"items-end\" : \"items-center\"\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{nestLabel ? tooltipLabel : null}\n\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"text-muted-foreground\",\n\t\t\t\t\t\t\t\t\t\t\t\t\ttruncate ? \"max-w-40 truncate leading-normal -my-1\" : \"\"\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{itemConfig?.label || item.name}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t{item.value !== undefined && (\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"font-medium text-foreground\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{content && typeof content === \"function\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? content(item, key)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: item.value.toLocaleString() + (unit ? unit : \"\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{totalValueDisplay ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<Separator className=\"mt-0.5\" />\n\t\t\t\t\t\t\t<div className=\"flex items-center justify-between gap-2 -mt-0.75 font-medium\">\n\t\t\t\t\t\t\t\t<span className=\"text-muted-foreground ps-3\">{totalLabelNode}</span>\n\t\t\t\t\t\t\t\t<span>{totalValueDisplay}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t)\n\t}\n)\nChartTooltipContent.displayName = \"ChartTooltip\"\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nconst ChartLegendContent = React.forwardRef<\n\tHTMLDivElement,\n\tReact.ComponentProps<\"div\"> &\n\t\tPick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n\t\t\thideIcon?: boolean\n\t\t\tnameKey?: string\n\t\t\treverse?: boolean\n\t\t}\n>(({ className, payload, verticalAlign = \"bottom\", reverse = false }, ref) => {\n\t// const { config } = useChart()\n\n\tif (!payload?.length) {\n\t\treturn null\n\t}\n\n\tconst reversedPayload = reverse ? [...payload].reverse() : payload\n\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"flex items-center justify-center gap-4 gap-y-1 flex-wrap ps-4\",\n\t\t\t\tverticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t>\n\t\t\t{reversedPayload.map((item) => {\n\t\t\t\t// const key = `${nameKey || item.dataKey || 'value'}`\n\t\t\t\t// const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n\t\t\t\treturn (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={item.value}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t// 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground text-muted-foreground'\n\t\t\t\t\t\t\t\"flex items-center gap-1.5 text-muted-foreground\"\n\t\t\t\t\t\t)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{/* {itemConfig?.icon && !hideIcon ? (\n\t\t\t\t\t\t\t<itemConfig.icon />\n\t\t\t\t\t\t) : ( */}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"h-2 w-2 shrink-0 rounded-[2px]\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tbackgroundColor: item.color,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{item.value}\n\t\t\t\t\t\t{/* )} */}\n\t\t\t\t\t\t{/* {itemConfig?.label} */}\n\t\t\t\t\t</div>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n})\nChartLegendContent.displayName = \"ChartLegend\"\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n\tif (typeof payload !== \"object\" || payload === null) {\n\t\treturn undefined\n\t}\n\n\tconst payloadPayload =\n\t\t\"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null\n\t\t\t? payload.payload\n\t\t\t: undefined\n\n\tlet configLabelKey: string = key\n\n\tif (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n\t\tconfigLabelKey = payload[key as keyof typeof payload] as string\n\t} else if (\n\t\tpayloadPayload &&\n\t\tkey in payloadPayload &&\n\t\ttypeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n\t) {\n\t\tconfigLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string\n\t}\n\n\treturn configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]\n}\n\nlet cachedAxis: JSX.Element\nconst xAxis = ({ domain, ticks, chartTime }: ChartData) => {\n\tif (cachedAxis && domain[0] === cachedAxis.props.domain[0]) {\n\t\treturn cachedAxis\n\t}\n\tcachedAxis = (\n\t\t<RechartsPrimitive.XAxis\n\t\t\tdataKey=\"created\"\n\t\t\tdomain={domain}\n\t\t\tticks={ticks}\n\t\t\tallowDataOverflow\n\t\t\ttype=\"number\"\n\t\t\tscale=\"time\"\n\t\t\tminTickGap={12}\n\t\t\ttickMargin={8}\n\t\t\taxisLine={false}\n\t\t\ttickFormatter={chartTimeData[chartTime].format}\n\t\t/>\n\t)\n\treturn cachedAxis\n}\n\nexport {\n\tChartContainer,\n\tChartTooltip,\n\tChartTooltipContent,\n\tChartLegend,\n\tChartLegendContent,\n\txAxis,\n\t// ChartStyle,\n}\n\nexport function pinnedAxisDomain(): AxisDomain {\n\treturn [\n\t\t0,\n\t\t(dataMax: number) => {\n\t\t\tif (dataMax > 10) {\n\t\t\t\treturn Math.round(dataMax)\n\t\t\t}\n\t\t\tif (dataMax > 1) {\n\t\t\t\treturn Math.round(dataMax / 0.1) * 0.1\n\t\t\t}\n\t\t\treturn dataMax\n\t\t},\n\t]\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/checkbox.tsx",
    "content": "\"use client\"\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n\tReact.ElementRef<typeof CheckboxPrimitive.Root>,\n\tReact.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n\t<CheckboxPrimitive.Root\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"peer size-4 flex items-center justify-center shrink-0 rounded-[.3em] border border-input ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t<CheckboxPrimitive.Indicator className={cn(\"flex items-center justify-center text-current\")}>\n\t\t\t<Check className=\"size-4\" />\n\t\t</CheckboxPrimitive.Indicator>\n\t</CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "internal/site/src/components/ui/collapsible.tsx",
    "content": "import { ChevronDownIcon, HourglassIcon } from \"lucide-react\"\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\n\ninterface CollapsibleProps {\n\ttitle: string\n\tchildren: React.ReactNode\n\tdescription?: React.ReactNode\n\tdefaultOpen?: boolean\n\tclassName?: string\n\ticon?: React.ReactNode\n}\n\nexport function Collapsible({ title, children, description, defaultOpen = false, className, icon }: CollapsibleProps) {\n\tconst [isOpen, setIsOpen] = React.useState(defaultOpen)\n\n\treturn (\n\t\t<div className={cn(\"border rounded-lg\", className)}>\n\t\t\t<Button variant=\"ghost\" className=\"w-full justify-between p-4 font-semibold\" onClick={() => setIsOpen(!isOpen)}>\n\t\t\t\t<div className=\"flex items-center gap-2\">\n\t\t\t\t\t{icon}\n\t\t\t\t\t{title}\n\t\t\t\t</div>\n\t\t\t\t<ChevronDownIcon\n\t\t\t\t\tclassName={cn(\"h-4 w-4 transition-transform duration-200\", {\n\t\t\t\t\t\t\"rotate-180\": isOpen,\n\t\t\t\t\t})}\n\t\t\t\t/>\n\t\t\t</Button>\n\t\t\t{description && <div className=\"px-4 pb-2 text-sm text-muted-foreground\">{description}</div>}\n\t\t\t{isOpen && (\n\t\t\t\t<div className=\"px-4 pb-4\">\n\t\t\t\t\t<div className=\"grid gap-3\">{children}</div>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/command.tsx",
    "content": "import { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } from \"lucide-react\"\nimport type * as React from \"react\"\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { cn } from \"@/lib/utils\"\n\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {\n\treturn (\n\t\t<CommandPrimitive\n\t\t\tdata-slot=\"command\"\n\t\t\tclassName={cn(\"bg-card flex h-full w-full flex-col overflow-hidden rounded-md\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CommandDialog({\n\ttitle = \"Command Palette\",\n\tdescription = \"Search for a command to run...\",\n\tchildren,\n\tclassName,\n\tshowCloseButton = true,\n\t...props\n}: React.ComponentProps<typeof Dialog> & {\n\ttitle?: string\n\tdescription?: string\n\tclassName?: string\n\tshowCloseButton?: boolean\n}) {\n\treturn (\n\t\t<Dialog {...props}>\n\t\t\t<DialogHeader className=\"sr-only\">\n\t\t\t\t<DialogTitle>{title}</DialogTitle>\n\t\t\t\t<DialogDescription>{description}</DialogDescription>\n\t\t\t</DialogHeader>\n\t\t\t<DialogContent className={cn(\"overflow-hidden p-0\", className)}>\n\t\t\t\t<Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n\t\t\t\t\t{children}\n\t\t\t\t</Command>\n\t\t\t</DialogContent>\n\t\t</Dialog>\n\t)\n}\n\nfunction CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {\n\treturn (\n\t\t<div data-slot=\"command-input-wrapper\" className=\"flex h-9 items-center gap-2 border-b px-3\">\n\t\t\t<SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n\t\t\t<CommandPrimitive.Input\n\t\t\t\tdata-slot=\"command-input\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {\n\treturn (\n\t\t<CommandPrimitive.List\n\t\t\tdata-slot=\"command-list\"\n\t\t\tclassName={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n\treturn <CommandPrimitive.Empty data-slot=\"command-empty\" className=\"py-6 text-center text-sm\" {...props} />\n}\n\nfunction CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {\n\treturn (\n\t\t<CommandPrimitive.Group\n\t\t\tdata-slot=\"command-group\"\n\t\t\tclassName={cn(\n\t\t\t\t\"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n\treturn (\n\t\t<CommandPrimitive.Separator\n\t\t\tdata-slot=\"command-separator\"\n\t\t\tclassName={cn(\"bg-border -mx-1 h-px\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n\treturn (\n\t\t<CommandPrimitive.Item\n\t\t\tdata-slot=\"command-item\"\n\t\t\tclassName={cn(\n\t\t\t\t\"data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n\treturn (\n\t\t<span\n\t\t\tdata-slot=\"command-shortcut\"\n\t\t\tclassName={cn(\"text-muted-foreground ml-auto text-xs tracking-wide\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport {\n\tCommand,\n\tCommandDialog,\n\tCommandInput,\n\tCommandList,\n\tCommandEmpty,\n\tCommandGroup,\n\tCommandItem,\n\tCommandShortcut,\n\tCommandSeparator,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/dialog.tsx",
    "content": "import * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n\tReact.ElementRef<typeof DialogPrimitive.Overlay>,\n\tReact.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n\t<DialogPrimitive.Overlay\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n\tReact.ElementRef<typeof DialogPrimitive.Content>,\n\tReact.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n\t<DialogPortal>\n\t\t<DialogOverlay />\n\t\t<DialogPrimitive.Content\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t\t<DialogPrimitive.Close className=\"absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n\t\t\t\t<X className=\"h-4 w-4\" />\n\t\t\t\t<span className=\"sr-only\">Close</span>\n\t\t\t</DialogPrimitive.Close>\n\t\t</DialogPrimitive.Content>\n\t</DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n\t<div className={cn(\"grid gap-1.5 text-center sm:text-start\", className)} {...props} />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n\t<div className={cn(\"flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-3.5\", className)} {...props} />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n\tReact.ElementRef<typeof DialogPrimitive.Title>,\n\tReact.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n\t<DialogPrimitive.Title\n\t\tref={ref}\n\t\tclassName={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n\t\t{...props}\n\t/>\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n\tReact.ElementRef<typeof DialogPrimitive.Description>,\n\tReact.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n\t<DialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n\tDialog,\n\tDialogPortal,\n\tDialogOverlay,\n\tDialogClose,\n\tDialogTrigger,\n\tDialogContent,\n\tDialogHeader,\n\tDialogFooter,\n\tDialogTitle,\n\tDialogDescription,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/dropdown-menu.tsx",
    "content": "import * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n\t\tinset?: boolean\n\t}\n>(({ className, inset, children, ...props }, ref) => (\n\t<DropdownMenuPrimitive.SubTrigger\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 data-[state=open]:bg-accent/70\",\n\t\t\tinset && \"ps-8\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t{children}\n\t\t<ChevronRight className=\"ms-auto h-4 w-4\" />\n\t</DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n\t<DropdownMenuPrimitive.SubContent\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.Content>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n\t<DropdownMenuPrimitive.Portal>\n\t\t<DropdownMenuPrimitive.Content\n\t\t\tref={ref}\n\t\t\tsideOffset={sideOffset}\n\t\t\tclassName={cn(\n\t\t\t\t\"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t</DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.Item>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n\t\tinset?: boolean\n\t}\n>(({ className, inset, ...props }, ref) => (\n\t<DropdownMenuPrimitive.Item\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"cursor-pointer relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n\t\t\tinset && \"ps-8\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n\t<DropdownMenuPrimitive.CheckboxItem\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n\t\t\tclassName\n\t\t)}\n\t\tchecked={checked}\n\t\t{...props}\n\t>\n\t\t<span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n\t\t\t<DropdownMenuPrimitive.ItemIndicator>\n\t\t\t\t<Check className=\"h-4 w-4\" />\n\t\t\t</DropdownMenuPrimitive.ItemIndicator>\n\t\t</span>\n\t\t{children}\n\t</DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n\t<DropdownMenuPrimitive.RadioItem\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t<span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n\t\t\t<DropdownMenuPrimitive.ItemIndicator>\n\t\t\t\t<Circle className=\"h-2 w-2 fill-current\" />\n\t\t\t</DropdownMenuPrimitive.ItemIndicator>\n\t\t</span>\n\t\t{children}\n\t</DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.Label>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n\t\tinset?: boolean\n\t}\n>(({ className, inset, ...props }, ref) => (\n\t<DropdownMenuPrimitive.Label\n\t\tref={ref}\n\t\tclassName={cn(\"px-2.5 py-1.5 text-sm font-semibold\", inset && \"ps-8\", className)}\n\t\t{...props}\n\t/>\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n\tReact.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n\tReact.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n\t<DropdownMenuPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-muted\", className)} {...props} />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n\treturn <span className={cn(\"ms-auto text-xs tracking-widest opacity-60\", className)} {...props} />\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n\tDropdownMenu,\n\tDropdownMenuTrigger,\n\tDropdownMenuContent,\n\tDropdownMenuItem,\n\tDropdownMenuCheckboxItem,\n\tDropdownMenuRadioItem,\n\tDropdownMenuLabel,\n\tDropdownMenuSeparator,\n\tDropdownMenuShortcut,\n\tDropdownMenuGroup,\n\tDropdownMenuPortal,\n\tDropdownMenuSub,\n\tDropdownMenuSubContent,\n\tDropdownMenuSubTrigger,\n\tDropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/icons.tsx",
    "content": "import type { SVGProps } from \"react\"\n\n// linux-logo-bold from https://github.com/phosphor-icons/core (MIT license)\nexport function TuxIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 256 256\" {...props}>\n\t\t\t<path\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\td=\"M231 217a12 12 0 0 1-16-2c-2-1-35-44-35-127a52 52 0 1 0-104 0c0 83-33 126-35 127a12 12 0 0 1-18-14c0-1 29-39 29-113a76 76 0 1 1 152 0c0 74 29 112 29 113a12 12 0 0 1-2 16m-127-97a16 16 0 1 0-16-16 16 16 0 0 0 16 16m64-16a16 16 0 1 0-16 16 16 16 0 0 0 16-16m-73 51 28 12a12 12 0 0 0 10 0l28-12a12 12 0 0 0-10-22l-23 10-23-10a12 12 0 0 0-10 22m33 29a57 57 0 0 0-39 15 12 12 0 0 0 17 18 33 33 0 0 1 44 0 12 12 0 1 0 17-18 57 57 0 0 0-39-15\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\n// icon park (Apache 2.0) https://github.com/bytedance/IconPark/blob/master/LICENSE\nexport function WindowsIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg {...props} viewBox=\"0 0 48 48\">\n\t\t\t<path\n\t\t\t\tfill=\"none\"\n\t\t\t\tstroke=\"currentColor\"\n\t\t\t\tstrokeWidth=\"3.8\"\n\t\t\t\td=\"m6.8 11 12.9-1.7v12.1h-13zm18-2.2 16.4-2v14.6H25zm0 18.6 16.4.4v13.4L25 38.6zm-18-.8 12.9.3v10.9l-13-2.2z\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\n// teenyicons (MIT) https://github.com/teenyicons/teenyicons/blob/master/LICENSE\nexport function AppleIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 20 20\" {...props}>\n\t\t\t<path\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\td=\"M14.1 4.7a5 5 0 0 1 3.8 2c-3.3 1.9-2.8 6.7.6 8L17.2 17c-.8 1.3-2 2.9-3.5 2.9-1.2 0-1.6-.9-3.3-.8s-2.2.8-3.5.8c-1.4 0-2.5-1.5-3.4-2.7-2.3-3.6-2.5-7.9-1.1-10 1-1.7 2.6-2.6 4.1-2.6 1.6 0 2.6.8 3.8.8 1.3 0 2-.8 3.8-.8M13.7 0c.2 1.2-.3 2.4-1 3.2a4 4 0 0 1-3 1.6c-.2-1.2.3-2.3 1-3.2.7-.8 2-1.5 3-1.6\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function FreeBsdIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props}>\n\t\t\t<path\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\td=\"M2.7 2C3.5 2 6 3.2 6 3.2 4.8 4 3.7 5 3 6.4 2.1 4.8 1.3 2.9 2 2.2l.7-.2m18.1.1c.4 0 .8 0 1 .2 1 1.1-2 5.8-2.4 6.4-.5.5-1.8 0-2.9-1-1-1.2-1.5-2.4-1-3 .4-.4 3.6-2.4 5.3-2.6m-8.8.5c1.3 0 2.5.2 3.7.7l-1 .7c-1 1-.6 2.8 1 4.4 1 1 2.1 1.6 3 1.6a2 2 0 0 0 1.5-.6l.7-1a9.7 9.7 0 1 1-18.6 3.8A9.7 9.7 0 0 1 12 2.7\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\n// ion icons (MIT) https://github.com/ionic-team/ionicons/blob/main/LICENSE\nexport function DockerIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg {...props} viewBox=\"0 0 512 512\" fill=\"currentColor\">\n\t\t\t<path d=\"M507 211c-1-1-14-11-42-11a133 133 0 0 0-21 2c-6-36-36-54-37-55l-7-4-5 7a102 102 0 0 0-13 30c-5 21-2 40 8 57-12 7-33 9-37 9H16a16 16 0 0 0-16 16 241 241 0 0 0 15 87c11 30 29 53 51 67 25 15 66 24 113 24a344 344 0 0 0 62-6 257 257 0 0 0 82-29 224 224 0 0 0 55-46c27-30 43-64 55-94h4c30 0 48-12 58-22a63 63 0 0 0 15-22l2-6Z\" />\n\t\t\t<path d=\"M47 236h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4H47a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m-125-57h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m63 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m62 0h45a4 4 0 0 0 4-4v-41a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v41a4 4 0 0 0 4 4m0-58h45a4 4 0 0 0 4-4V76a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4m63 116h45a4 4 0 0 0 4-4v-40a4 4 0 0 0-4-4h-45a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4\" />\n\t\t</svg>\n\t)\n}\n\n// MingCute Apache License 2.0 https://github.com/Richard9394/MingCute\nexport function Rows(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props}>\n\t\t\t<path\n\t\t\t\tfill=\"currentColor\"\n\t\t\t\td=\"M5 3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 2h14v4H5zm0 8a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2zm0 2h14v4H5z\"\n\t\t\t/>\n\t\t</svg>\n\t)\n}\n\n// IconPark Apache License 2.0 https://github.com/bytedance/IconPark\nexport function ChartAverage(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg fill=\"none\" viewBox=\"0 0 48 48\" stroke=\"currentColor\" {...props}>\n\t\t\t<path strokeWidth=\"3\" d=\"M4 4v40h40\" />\n\t\t\t<path strokeWidth=\"3\" d=\"M10 38S15.3 4 27 4s17 34 17 34\" />\n\t\t\t<path strokeWidth=\"4\" d=\"M10 24h34\" />\n\t\t</svg>\n\t)\n}\n\n// IconPark Apache License 2.0 https://github.com/bytedance/IconPark\nexport function ChartMax(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg fill=\"none\" viewBox=\"0 0 48 48\" stroke=\"currentColor\" {...props}>\n\t\t\t<path strokeWidth=\"3\" d=\"M4 4v40h40\" />\n\t\t\t<path strokeWidth=\"3\" d=\"M10 38S15.3 4 27 4s17 34 17 34\" />\n\t\t\t<path strokeWidth=\"4\" d=\"M10 4h34\" />\n\t\t</svg>\n\t)\n}\n\n// Lucide https://github.com/lucide-icons/lucide (not in package for some reason)\nexport function EthernetIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg fill=\"none\" stroke=\"currentColor\" strokeLinecap=\"round\" strokeWidth=\"2\" viewBox=\"0 0 24 24\" {...props}>\n\t\t\t<path d=\"m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3zM6 8v1m4-1v1m4-1v1m4-1v1\" />\n\t\t</svg>\n\t)\n}\n\n// Phosphor MIT https://github.com/phosphor-icons/core\nexport function ThermometerIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 256 256\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M212 56a28 28 0 1 0 28 28 28 28 0 0 0-28-28m0 40a12 12 0 1 1 12-12 12 12 0 0 1-12 12m-60 50V40a32 32 0 0 0-64 0v106a56 56 0 1 0 64 0m-16-42h-32V40a16 16 0 0 1 32 0Z\" />\n\t\t</svg>\n\t)\n}\n\n// Huge icons (MIT)\nexport function GpuIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} stroke=\"currentColor\" fill=\"none\" strokeWidth=\"2\">\n\t\t\t<path d=\"M4 21V4.1a1.5 1.5 0 0 0-1.1-1L2 3m2 2h13c2.4 0 3.5 0 4.3.7s.7 2 .7 4.3v4.5c0 2.4 0 3.5-.7 4.3-.8.7-2 .7-4.3.7h-4.9a1.8 1.8 0 0 1-1.6-1c-.3-.6-1-1-1.6-1H4\" />\n\t\t\t<path d=\"M19 11.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0m-11.5-3h2m-2 3h2m-2 3h2\" />\n\t\t</svg>\n\t)\n}\n\n// Remix icons (Apache 2.0) https://github.com/Remix-Design/RemixIcon/blob/master/License\nexport function HourglassIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M4 2h16v4.5L13.5 12l6.5 5.5V22H4v-4.5l6.5-5.5L4 6.5zm12.3 5L18 5.5V4H6v1.5L7.7 7zM12 13.3l-6 5.2V20h1l5-3 5 3h1v-1.5z\" />\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function WebSocketIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 256 193\" {...props} fill=\"currentColor\">\n\t\t\t<title>WebSocket</title>\n\t\t\t<path d=\"M192 145h32V68l-36-35-22 22 26 27zm32 16H113l-26-27 11-11 22 22h45l-44-45 11-11 44 44V88l-21-22 11-11-55-55H0l32 32h65l24 23-34 34-24-23V48H32v31l55 55-23 22 36 36h156z\" />\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function BatteryMediumIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M16 13H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4\" />\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function BatteryLowIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M16 17H8V6h8m.7-2H15V2H9v2H7.3A1.3 1.3 0 0 0 6 5.3v15.4q.1 1.2 1.3 1.3h9.4a1.3 1.3 0 0 0 1.3-1.3V5.3q-.1-1.2-1.3-1.3\" />\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function BatteryHighIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M16 9H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4\" />\n\t\t</svg>\n\t)\n}\n\n// Apache 2.0 https://github.com/Templarian/MaterialDesign/blob/master/LICENSE\nexport function BatteryFullIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M16.67 4H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4\" />\n\t\t</svg>\n\t)\n}\n\n// https://github.com/phosphor-icons/core (MIT license)\nexport function PlugChargingIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 256 256\" {...props} fill=\"currentColor\">\n\t\t\t<path d=\"M224,48H180V16a12,12,0,0,0-24,0V48H100V16a12,12,0,0,0-24,0V48H32.55C24.4,48,20,54.18,20,60A12,12,0,0,0,32,72H44v92a44.05,44.05,0,0,0,44,44h28v32a12,12,0,0,0,24,0V208h28a44.05,44.05,0,0,0,44-44V72h12a12,12,0,0,0,0-24ZM188,164a20,20,0,0,1-20,20H88a20,20,0,0,1-20-20V72H188Zm-85.86-29.17a12,12,0,0,1-1.38-11l12-32a12,12,0,1,1,22.48,8.42L129.32,116H144a12,12,0,0,1,11.24,16.21l-12,32a12,12,0,0,1-22.48-8.42L126.68,140H112A12,12,0,0,1,102.14,134.83Z\" />\n\t\t</svg>\n\t)\n}\n\n// Lucide Icons (ISC) - used for ports\nexport function SquareArrowRightEnterIcon(props: SVGProps<SVGSVGElement>) {\n\treturn (\n\t\t<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" {...props}>\n\t\t\t<path d=\"m10 16 4-4-4-4\" />\n\t\t\t<path d=\"M3 12h11\" />\n\t\t\t<path d=\"M3 8V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3\" />\n\t\t</svg>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/input-copy.tsx",
    "content": "import { Trans } from \"@lingui/react/macro\"\nimport { CopyIcon } from \"lucide-react\"\nimport { copyToClipboard } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport { Input } from \"./input\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./tooltip\"\n\nexport function InputCopy({ value, id, name }: { value: string; id: string; name: string }) {\n\treturn (\n\t\t<div className=\"relative\">\n\t\t\t<Input readOnly id={id} name={name} value={value} required></Input>\n\t\t\t<div\n\t\t\t\tclassName={\n\t\t\t\t\t\"h-6 w-24 bg-linear-to-r rtl:bg-linear-to-l from-transparent to-background to-65% absolute top-2 end-1 pointer-events-none\"\n\t\t\t\t}\n\t\t\t></div>\n\t\t\t<Tooltip disableHoverableContent={true}>\n\t\t\t\t<TooltipTrigger asChild>\n\t\t\t\t\t<Button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tvariant={\"link\"}\n\t\t\t\t\t\tclassName=\"absolute end-0 top-0\"\n\t\t\t\t\t\tonClick={() => copyToClipboard(value)}\n\t\t\t\t\t>\n\t\t\t\t\t\t<CopyIcon className=\"size-4\" />\n\t\t\t\t\t</Button>\n\t\t\t\t</TooltipTrigger>\n\t\t\t\t<TooltipContent>\n\t\t\t\t\t<p>\n\t\t\t\t\t\t<Trans>Click to copy</Trans>\n\t\t\t\t\t</p>\n\t\t\t\t</TooltipContent>\n\t\t\t</Tooltip>\n\t\t</div>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/input-tags.tsx",
    "content": "import { XIcon } from \"lucide-react\"\nimport * as React from \"react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport type { InputProps } from \"./input\"\n\ntype InputTagsProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n\tvalue: string[]\n\tonChange: React.Dispatch<React.SetStateAction<string[]>>\n}\n\nconst InputTags = React.forwardRef<HTMLInputElement, InputTagsProps>(\n\t({ className, value, onChange, ...props }, ref) => {\n\t\tconst [pendingDataPoint, setPendingDataPoint] = React.useState(\"\")\n\n\t\tReact.useEffect(() => {\n\t\t\tif (pendingDataPoint.includes(\",\")) {\n\t\t\t\tconst newDataPoints = new Set([...value, ...pendingDataPoint.split(\",\").map((chunk) => chunk.trim())])\n\t\t\t\tonChange(Array.from(newDataPoints))\n\t\t\t\tsetPendingDataPoint(\"\")\n\t\t\t}\n\t\t}, [pendingDataPoint, onChange, value])\n\n\t\tconst addPendingDataPoint = () => {\n\t\t\tif (pendingDataPoint) {\n\t\t\t\tconst newDataPoints = new Set([...value, pendingDataPoint])\n\t\t\t\tonChange(Array.from(newDataPoints))\n\t\t\t\tsetPendingDataPoint(\"\")\n\t\t\t}\n\t\t}\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"bg-background min-h-10 flex w-full flex-wrap gap-2 rounded-md border px-3 py-2 text-sm  placeholder:text-muted-foreground has-focus-visible:outline-hidden ring-offset-background has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{value.map((item) => (\n\t\t\t\t\t<Badge key={item}>\n\t\t\t\t\t\t{item}\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tclassName=\"ms-2 h-3 w-3\"\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tonChange(value.filter((i) => i !== item))\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<XIcon className=\"w-3\" />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</Badge>\n\t\t\t\t))}\n\t\t\t\t<input\n\t\t\t\t\tclassName=\"flex-1 outline-hidden bg-background placeholder:text-muted-foreground\"\n\t\t\t\t\tvalue={pendingDataPoint}\n\t\t\t\t\tonChange={(e) => setPendingDataPoint(e.target.value)}\n\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\tif (e.key === \"Enter\" || e.key === \",\") {\n\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\taddPendingDataPoint()\n\t\t\t\t\t\t} else if (e.key === \"Backspace\" && pendingDataPoint.length === 0 && value.length > 0) {\n\t\t\t\t\t\t\te.preventDefault()\n\t\t\t\t\t\t\tonChange(value.slice(0, -1))\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\t{...props}\n\t\t\t\t\tref={ref}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t)\n\t}\n)\n\nInputTags.displayName = \"InputTags\"\n\nexport { InputTags }\n"
  },
  {
    "path": "internal/site/src/components/ui/input.tsx",
    "content": "import type * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n\treturn (\n\t\t<input\n\t\t\ttype={type}\n\t\t\tdata-slot=\"input\"\n\t\t\tclassName={cn(\n\t\t\t\t\"flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\t\"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Input }\n"
  },
  {
    "path": "internal/site/src/components/ui/label.tsx",
    "content": "import * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\")\n\nconst Label = React.forwardRef<\n\tReact.ElementRef<typeof LabelPrimitive.Root>,\n\tReact.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n\t<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "internal/site/src/components/ui/otp.tsx",
    "content": "import { OTPInput, OTPInputContext } from \"input-otp\"\nimport { MinusIcon } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction InputOTP({\n\tclassName,\n\tcontainerClassName,\n\t...props\n}: React.ComponentProps<typeof OTPInput> & {\n\tcontainerClassName?: string\n}) {\n\treturn (\n\t\t<OTPInput\n\t\t\tdata-slot=\"input-otp\"\n\t\t\tcontainerClassName={cn(\"flex items-center gap-2 has-disabled:opacity-50\", containerClassName)}\n\t\t\tclassName={cn(\"disabled:cursor-not-allowed\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction InputOTPGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n\treturn <div data-slot=\"input-otp-group\" className={cn(\"flex items-center\", className)} {...props} />\n}\n\nfunction InputOTPSlot({\n\tindex,\n\tclassName,\n\t...props\n}: React.ComponentProps<\"div\"> & {\n\tindex: number\n}) {\n\tconst inputOTPContext = React.useContext(OTPInputContext)\n\tconst { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}\n\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"input-otp-slot\"\n\t\t\tdata-active={isActive}\n\t\t\tclassName={cn(\n\t\t\t\t\"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{char}\n\t\t\t{hasFakeCaret && (\n\t\t\t\t<div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n\t\t\t\t\t<div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n\nfunction InputOTPSeparator({ ...props }: React.ComponentProps<\"div\">) {\n\treturn (\n\t\t<div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n\t\t\t<MinusIcon />\n\t\t</div>\n\t)\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
  },
  {
    "path": "internal/site/src/components/ui/select.tsx",
    "content": "import * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.Trigger>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n\t<SelectPrimitive.Trigger\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t{children}\n\t\t<SelectPrimitive.Icon asChild>\n\t\t\t<ChevronDown className=\"h-4 w-4 opacity-50\" />\n\t\t</SelectPrimitive.Icon>\n\t</SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n\t<SelectPrimitive.ScrollUpButton\n\t\tref={ref}\n\t\tclassName={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n\t\t{...props}\n\t>\n\t\t<ChevronUp className=\"h-4 w-4\" />\n\t</SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n\t<SelectPrimitive.ScrollDownButton\n\t\tref={ref}\n\t\tclassName={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n\t\t{...props}\n\t>\n\t\t<ChevronDown className=\"h-4 w-4\" />\n\t</SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.Content>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n\t<SelectPrimitive.Portal>\n\t\t<SelectPrimitive.Content\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n\t\t\t\tposition === \"popper\" &&\n\t\t\t\t\t\"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tposition={position}\n\t\t\t{...props}\n\t\t>\n\t\t\t<SelectScrollUpButton />\n\t\t\t<SelectPrimitive.Viewport\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"p-1\",\n\t\t\t\t\tposition === \"popper\" && \"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)\"\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</SelectPrimitive.Viewport>\n\t\t\t<SelectScrollDownButton />\n\t\t</SelectPrimitive.Content>\n\t</SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.Label>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n\t<SelectPrimitive.Label ref={ref} className={cn(\"py-1.5 ps-8 pe-2 text-sm font-semibold\", className)} {...props} />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.Item>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n\t<SelectPrimitive.Item\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t>\n\t\t<span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n\t\t\t<SelectPrimitive.ItemIndicator>\n\t\t\t\t<Check className=\"h-4 w-4\" />\n\t\t\t</SelectPrimitive.ItemIndicator>\n\t\t</span>\n\n\t\t<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n\t</SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n\tReact.ElementRef<typeof SelectPrimitive.Separator>,\n\tReact.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n\t<SelectPrimitive.Separator ref={ref} className={cn(\"-mx-1 my-1 h-px bg-muted\", className)} {...props} />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n\tSelect,\n\tSelectGroup,\n\tSelectValue,\n\tSelectTrigger,\n\tSelectContent,\n\tSelectLabel,\n\tSelectItem,\n\tSelectSeparator,\n\tSelectScrollUpButton,\n\tSelectScrollDownButton,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/separator.tsx",
    "content": "import * as SeparatorPrimitive from \"@radix-ui/react-separator\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Separator = React.forwardRef<\n\tReact.ElementRef<typeof SeparatorPrimitive.Root>,\n\tReact.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = \"horizontal\", decorative = true, ...props }, ref) => (\n\t<SeparatorPrimitive.Root\n\t\tref={ref}\n\t\tdecorative={decorative}\n\t\torientation={orientation}\n\t\tclassName={cn(\"shrink-0 bg-border\", orientation === \"horizontal\" ? \"h-px w-full\" : \"h-full w-px\", className)}\n\t\t{...props}\n\t/>\n))\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "internal/site/src/components/ui/sheet.tsx",
    "content": "import * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\nimport type * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n\treturn <SheetPrimitive.Root data-slot=\"sheet\" {...props} />\n}\n\nfunction SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n\treturn <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\n\nfunction SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {\n\treturn <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\n\nfunction SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n\treturn <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\n\nfunction SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n\treturn (\n\t\t<SheetPrimitive.Overlay\n\t\t\tdata-slot=\"sheet-overlay\"\n\t\t\tclassName={cn(\n\t\t\t\t\"data-[state=open]:animate-in duration-500 isolate data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/40\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction SheetContent({\n\tclassName,\n\tchildren,\n\tside = \"right\",\n\t...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n\tside?: \"top\" | \"right\" | \"bottom\" | \"left\"\n}) {\n\treturn (\n\t\t<SheetPortal>\n\t\t\t<SheetOverlay />\n\t\t\t<SheetPrimitive.Content\n\t\t\t\tdata-slot=\"sheet-content\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-[400ms]\",\n\t\t\t\t\tside === \"right\" &&\n\t\t\t\t\t\t\"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n\t\t\t\t\tside === \"left\" &&\n\t\t\t\t\t\t\"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n\t\t\t\t\tside === \"top\" &&\n\t\t\t\t\t\t\"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n\t\t\t\t\tside === \"bottom\" &&\n\t\t\t\t\t\t\"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n\t\t\t\t\t<XIcon className=\"size-4\" />\n\t\t\t\t\t<span className=\"sr-only\">Close</span>\n\t\t\t\t</SheetPrimitive.Close>\n\t\t\t</SheetPrimitive.Content>\n\t\t</SheetPortal>\n\t)\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n\treturn <div data-slot=\"sheet-header\" className={cn(\"flex flex-col gap-1.5 p-4\", className)} {...props} />\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n\treturn <div data-slot=\"sheet-footer\" className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />\n}\n\nfunction SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {\n\treturn (\n\t\t<SheetPrimitive.Title\n\t\t\tdata-slot=\"sheet-title\"\n\t\t\tclassName={cn(\"text-foreground font-semibold\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nfunction SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {\n\treturn (\n\t\t<SheetPrimitive.Description\n\t\t\tdata-slot=\"sheet-description\"\n\t\t\tclassName={cn(\"text-muted-foreground text-sm\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\nexport { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }\n"
  },
  {
    "path": "internal/site/src/components/ui/slider.tsx",
    "content": "import * as SliderPrimitive from \"@radix-ui/react-slider\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Slider = React.forwardRef<\n\tReact.ElementRef<typeof SliderPrimitive.Root>,\n\tReact.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n\t<SliderPrimitive.Root\n\t\tref={ref}\n\t\tclassName={cn(\"relative flex w-full touch-none select-none items-center\", className)}\n\t\t{...props}\n\t>\n\t\t<SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n\t\t\t<SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n\t\t</SliderPrimitive.Track>\n\t\t<SliderPrimitive.Thumb className=\"block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\" />\n\t</SliderPrimitive.Root>\n))\nSlider.displayName = SliderPrimitive.Root.displayName\n\nexport default Slider\n"
  },
  {
    "path": "internal/site/src/components/ui/switch.tsx",
    "content": "import * as SwitchPrimitives from \"@radix-ui/react-switch\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Switch = React.forwardRef<\n\tReact.ElementRef<typeof SwitchPrimitives.Root>,\n\tReact.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n\t<SwitchPrimitives.Root\n\t\tclassName={cn(\n\t\t\t\"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t\tref={ref}\n\t>\n\t\t<SwitchPrimitives.Thumb\n\t\t\tclassName={cn(\n\t\t\t\t\"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=checked]:rtl:-translate-x-5 data-[state=unchecked]:translate-x-0\"\n\t\t\t)}\n\t\t/>\n\t</SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n"
  },
  {
    "path": "internal/site/src/components/ui/table.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<div className=\"relative w-full overflow-auto\">\n\t\t\t<table ref={ref} className={cn(\"w-full caption-bottom text-sm\", className)} {...props} />\n\t\t</div>\n\t)\n)\nTable.displayName = \"Table\"\n\nconst TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<thead\n\t\t\tref={ref}\n\t\t\tclassName={cn(\"bg-table-header border-b border-border/50 [&_tr]:border-b\", className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n)\nTableHeader.displayName = \"TableHeader\"\n\nconst TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<tbody ref={ref} className={cn(\"[&_tr:last-child]:border-0\", className)} {...props} />\n\t)\n)\nTableBody.displayName = \"TableBody\"\n\nconst TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<tfoot ref={ref} className={cn(\"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0\", className)} {...props} />\n\t)\n)\nTableFooter.displayName = \"TableFooter\"\n\nconst TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<tr\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"border-b border-border/60 hover:bg-muted/40 dark:hover:bg-muted/20 data-[state=selected]:bg-muted!\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n)\nTableRow.displayName = \"TableRow\"\n\nconst TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<th\n\t\t\tref={ref}\n\t\t\tclassName={cn(\n\t\t\t\t\"h-12 px-4 text-start align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pe-0\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t)\n)\nTableHead.displayName = \"TableHead\"\n\nconst TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<td ref={ref} className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pe-0\", className)} {...props} />\n\t)\n)\nTableCell.displayName = \"TableCell\"\n\nconst TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(\n\t({ className, ...props }, ref) => (\n\t\t<caption ref={ref} className={cn(\"mt-4 text-sm text-muted-foreground\", className)} {...props} />\n\t)\n)\nTableCaption.displayName = \"TableCaption\"\n\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }\n"
  },
  {
    "path": "internal/site/src/components/ui/tabs.tsx",
    "content": "import * as TabsPrimitive from \"@radix-ui/react-tabs\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n\tReact.ElementRef<typeof TabsPrimitive.List>,\n\tReact.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n\t<TabsPrimitive.List\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n\tReact.ElementRef<typeof TabsPrimitive.Trigger>,\n\tReact.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n\t<TabsPrimitive.Trigger\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs cursor-pointer\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n\tReact.ElementRef<typeof TabsPrimitive.Content>,\n\tReact.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n\t<TabsPrimitive.Content\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "internal/site/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {\n\treturn (\n\t\t<textarea\n\t\t\tclassName={cn(\n\t\t\t\t\"flex min-h-14 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tref={ref}\n\t\t\t{...props}\n\t\t/>\n\t)\n})\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "internal/site/src/components/ui/toast.tsx",
    "content": "import * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Viewport>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n\t<ToastPrimitives.Viewport\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"fixed top-0 z-100 flex max-h-dvh w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n\t\"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pe-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"border bg-background text-foreground\",\n\t\t\t\tdestructive: \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t},\n\t}\n)\n\nconst Toast = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Root>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n\treturn <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Action>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n\t<ToastPrimitives.Action\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground focus:group-[.destructive]:ring-destructive\",\n\t\t\tclassName\n\t\t)}\n\t\t{...props}\n\t/>\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Close>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n\t<ToastPrimitives.Close\n\t\tref={ref}\n\t\tclassName={cn(\n\t\t\t\"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600\",\n\t\t\tclassName\n\t\t)}\n\t\ttoast-close=\"\"\n\t\t{...props}\n\t>\n\t\t<X className=\"h-4 w-4\" />\n\t</ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Title>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n\t<ToastPrimitives.Title ref={ref} className={cn(\"text-sm font-semibold\", className)} {...props} />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n\tReact.ElementRef<typeof ToastPrimitives.Description>,\n\tReact.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n\t<ToastPrimitives.Description ref={ref} className={cn(\"text-sm opacity-90\", className)} {...props} />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n\ttype ToastProps,\n\ttype ToastActionElement,\n\tToastProvider,\n\tToastViewport,\n\tToast,\n\tToastTitle,\n\tToastDescription,\n\tToastClose,\n\tToastAction,\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/toaster.tsx",
    "content": "import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from \"@/components/ui/toast\"\nimport { useToast } from \"@/components/ui/use-toast\"\n\nexport function Toaster() {\n\tconst { toasts } = useToast()\n\n\treturn (\n\t\t<ToastProvider>\n\t\t\t{toasts.map(({ id, title, description, action, ...props }) => (\n\t\t\t\t<Toast key={id} {...props}>\n\t\t\t\t\t<div className=\"grid gap-1\">\n\t\t\t\t\t\t{title && <ToastTitle>{title}</ToastTitle>}\n\t\t\t\t\t\t{description && <ToastDescription>{description}</ToastDescription>}\n\t\t\t\t\t</div>\n\t\t\t\t\t{action}\n\t\t\t\t\t<ToastClose />\n\t\t\t\t</Toast>\n\t\t\t))}\n\t\t\t<ToastViewport />\n\t\t</ToastProvider>\n\t)\n}\n"
  },
  {
    "path": "internal/site/src/components/ui/tooltip.tsx",
    "content": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport type * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction TooltipProvider({ delayDuration = 50, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n\treturn <TooltipPrimitive.Provider data-slot=\"tooltip-provider\" delayDuration={delayDuration} {...props} />\n}\n\nfunction Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n\treturn (\n\t\t<TooltipProvider>\n\t\t\t<TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n\t\t</TooltipProvider>\n\t)\n}\n\nfunction TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n\treturn <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n\tclassName,\n\tsideOffset = 0,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n\treturn (\n\t\t<TooltipPrimitive.Portal>\n\t\t\t<TooltipPrimitive.Content\n\t\t\t\tdata-slot=\"tooltip-content\"\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"bg-popover text-popover-foreground border animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<TooltipPrimitive.Arrow\n\t\t\t\t\tclassName=\"bg-popover border z-50 fill-popover size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] will-change-transform\"\n\t\t\t\t\tstyle={{ clipPath: \"inset(25% 0 0 25%)\" }}\n\t\t\t\t/>\n\t\t\t</TooltipPrimitive.Content>\n\t\t</TooltipPrimitive.Portal>\n\t)\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "internal/site/src/components/ui/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n\tid: string\n\ttitle?: React.ReactNode\n\tdescription?: React.ReactNode\n\taction?: ToastActionElement\n}\n\nconst actionTypes = {\n\tADD_TOAST: \"ADD_TOAST\",\n\tUPDATE_TOAST: \"UPDATE_TOAST\",\n\tDISMISS_TOAST: \"DISMISS_TOAST\",\n\tREMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n\tcount = (count + 1) % Number.MAX_SAFE_INTEGER\n\treturn count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n\t| {\n\t\t\ttype: ActionType[\"ADD_TOAST\"]\n\t\t\ttoast: ToasterToast\n\t  }\n\t| {\n\t\t\ttype: ActionType[\"UPDATE_TOAST\"]\n\t\t\ttoast: Partial<ToasterToast>\n\t  }\n\t| {\n\t\t\ttype: ActionType[\"DISMISS_TOAST\"]\n\t\t\ttoastId?: ToasterToast[\"id\"]\n\t  }\n\t| {\n\t\t\ttype: ActionType[\"REMOVE_TOAST\"]\n\t\t\ttoastId?: ToasterToast[\"id\"]\n\t  }\n\ninterface State {\n\ttoasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n\tif (toastTimeouts.has(toastId)) {\n\t\treturn\n\t}\n\n\tconst timeout = setTimeout(() => {\n\t\ttoastTimeouts.delete(toastId)\n\t\tdispatch({\n\t\t\ttype: \"REMOVE_TOAST\",\n\t\t\ttoastId: toastId,\n\t\t})\n\t}, TOAST_REMOVE_DELAY)\n\n\ttoastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n\tswitch (action.type) {\n\t\tcase \"ADD_TOAST\":\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n\t\t\t}\n\n\t\tcase \"UPDATE_TOAST\":\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),\n\t\t\t}\n\n\t\tcase \"DISMISS_TOAST\": {\n\t\t\tconst { toastId } = action\n\n\t\t\t// ! Side effects ! - This could be extracted into a dismissToast() action,\n\t\t\t// but I'll keep it here for simplicity\n\t\t\tif (toastId) {\n\t\t\t\taddToRemoveQueue(toastId)\n\t\t\t} else {\n\t\t\t\tstate.toasts.forEach((toast) => {\n\t\t\t\t\taddToRemoveQueue(toast.id)\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoasts: state.toasts.map((t) =>\n\t\t\t\t\tt.id === toastId || toastId === undefined\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...t,\n\t\t\t\t\t\t\t\topen: false,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: t\n\t\t\t\t),\n\t\t\t}\n\t\t}\n\t\tcase \"REMOVE_TOAST\":\n\t\t\tif (action.toastId === undefined) {\n\t\t\t\treturn {\n\t\t\t\t\t...state,\n\t\t\t\t\ttoasts: [],\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\ttoasts: state.toasts.filter((t) => t.id !== action.toastId),\n\t\t\t}\n\t}\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n\tmemoryState = reducer(memoryState, action)\n\tlisteners.forEach((listener) => {\n\t\tlistener(memoryState)\n\t})\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n\tconst id = genId()\n\n\tconst update = (props: ToasterToast) =>\n\t\tdispatch({\n\t\t\ttype: \"UPDATE_TOAST\",\n\t\t\ttoast: { ...props, id },\n\t\t})\n\tconst dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n\tdispatch({\n\t\ttype: \"ADD_TOAST\",\n\t\ttoast: {\n\t\t\t...props,\n\t\t\tid,\n\t\t\topen: true,\n\t\t\tonOpenChange: (open) => {\n\t\t\t\tif (!open) dismiss()\n\t\t\t},\n\t\t},\n\t})\n\n\treturn {\n\t\tid: id,\n\t\tdismiss,\n\t\tupdate,\n\t}\n}\n\nfunction useToast() {\n\tconst [state, setState] = React.useState<State>(memoryState)\n\n\tReact.useEffect(() => {\n\t\tlisteners.push(setState)\n\t\treturn () => {\n\t\t\tconst index = listeners.indexOf(setState)\n\t\t\tif (index > -1) {\n\t\t\t\tlisteners.splice(index, 1)\n\t\t\t}\n\t\t}\n\t}, [state])\n\n\treturn {\n\t\t...state,\n\t\ttoast,\n\t\tdismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n\t}\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "internal/site/src/index.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant light (&:is(.light *));\n@custom-variant dark (&:is(.dark *));\n@custom-variant safari (@supports (hanging-punctuation: first) and (-webkit-appearance: none));\n\n:root {\n\t--background: hsl(30 8% 98%);\n\t--foreground: hsl(30 0% 10%);\n\t--card: hsl(30 0% 100%);\n\t--card-foreground: hsl(240 6% 12%);\n\t--popover: hsl(30 0% 100%);\n\t--popover-foreground: hsl(240 10% 6.2%);\n\t--primary: hsl(240 5.88% 10%);\n\t--primary-foreground: hsl(30 0% 100%);\n\t--secondary: hsl(240 4.76% 95.88%);\n\t--secondary-foreground: hsl(240 5.88% 10%);\n\t--muted: hsl(26 6% 94%);\n\t--muted-foreground: hsl(24 2.79% 35.1%);\n\t--accent: hsl(20 23.08% 94%);\n\t--accent-foreground: hsl(240 5.88% 10%);\n\t--destructive: hsl(0 66% 53%);\n\t--destructive-foreground: hsl(0 0% 97%);\n\t--border: hsl(30 8.11% 85.49%);\n\t--input: hsl(30 4.29% 72.55%);\n\t--ring: hsl(30 3.97% 49.41%);\n\t--radius: 0.8rem;\n\t--chart-1: hsl(220 70% 50%);\n\t--chart-2: hsl(160 60% 45%);\n\t--chart-3: hsl(30 80% 55%);\n\t--chart-4: hsl(280 65% 60%);\n\t--chart-5: hsl(340 75% 55%);\n\t--table-header: hsl(225, 6%, 97%);\n\t--chart-saturation: 65%;\n\t--chart-lightness: 50%;\n\t--container: 1500px;\n}\n\n.dark {\n\tcolor-scheme: dark;\n\t--background: hsl(220 5.5% 9%);\n\t--foreground: hsl(220 2% 97%);\n\t--card: hsl(220 5.5% 10.5%);\n\t--card-foreground: hsl(220 2% 97%);\n\t--popover: hsl(220 5.5% 9%);\n\t--popover-foreground: hsl(220 2% 97%);\n\t--primary: hsl(220 2% 96%);\n\t--primary-foreground: hsl(220 4% 10%);\n\t--secondary: hsl(220 4% 16%);\n\t--secondary-foreground: hsl(220 0% 98%);\n\t--muted: hsl(220 6% 16%);\n\t--muted-foreground: hsl(220 4% 67%);\n\t--accent: hsl(220 5% 15.5%);\n\t--accent-foreground: hsl(220 2% 98%);\n\t--destructive: hsl(0 62% 46%);\n\t--border: hsl(220 3% 17%);\n\t--input: hsl(220 4% 22%);\n\t--ring: hsl(220 4% 80%);\n\t--table-header: hsl(220, 6%, 13%);\n\t--radius: 0.8rem;\n\t--chart-saturation: 60%;\n\t--chart-lightness: 55%;\n}\n\n@theme inline {\n\t--font-sans: Inter, InterVariable, sans-serif;\n\n\t--breakpoint-xs: 26.6rem;\n\t--breakpoint-450: 28rem;\n\t--breakpoint-2xl: 90rem;\n\n\t--radius-sm: calc(var(--radius) - 4px);\n\t--radius-md: calc(var(--radius) - 2px);\n\t--radius-lg: var(--radius);\n\t--radius-xl: calc(var(--radius) + 4px);\n\n\t--color-green-50: hsl(140 60% 95%);\n\t--color-green-100: hsl(140 50% 90%);\n\t--color-green-200: hsl(140 49% 80%);\n\t--color-green-300: hsl(140 48% 70%);\n\t--color-green-400: hsl(140 49% 60%);\n\t--color-green-500: hsl(140 50% 48%);\n\t--color-green-600: hsl(140 52% 38%);\n\t--color-green-700: hsl(140 53% 29%);\n\t--color-green-800: hsl(140 54% 20%);\n\t--color-green-900: hsl(140 54% 12%);\n\t--color-green-950: hsl(140 57% 6%);\n\n\t--color-gh-dark: #22272e;\n\n\t--color-background: var(--background);\n\t--color-foreground: var(--foreground);\n\t--color-card: var(--card);\n\t--color-card-foreground: var(--card-foreground);\n\t--color-popover: var(--popover);\n\t--color-popover-foreground: var(--popover-foreground);\n\t--color-primary: var(--primary);\n\t--color-primary-foreground: var(--primary-foreground);\n\t--color-secondary: var(--secondary);\n\t--color-secondary-foreground: var(--secondary-foreground);\n\t--color-muted: var(--muted);\n\t--color-muted-foreground: var(--muted-foreground);\n\t--color-accent: var(--accent);\n\t--color-accent-foreground: var(--accent-foreground);\n\t--color-destructive: var(--destructive);\n\t--color-destructive-foreground: var(--destructive-foreground);\n\t--color-border: var(--border);\n\t--color-input: var(--input);\n\t--color-ring: var(--ring);\n\t--color-chart-1: var(--chart-1);\n\t--color-chart-2: var(--chart-2);\n\t--color-chart-3: var(--chart-3);\n\t--color-chart-4: var(--chart-4);\n\t--color-chart-5: var(--chart-5);\n\t--color-table-header: var(--table-header);\n}\n\n@layer utilities {\n\t/* Fonts */\n\t@supports (font-variation-settings: normal) {\n\t\t:root {\n\t\t\tfont-family: Inter, InterVariable, sans-serif;\n\t\t}\n\t}\n\n\t@font-face {\n\t\tfont-family: InterVariable;\n\t\tfont-style: normal;\n\t\tfont-weight: 100 900;\n\t\tfont-display: swap;\n\t\tsrc: url(\"/static/InterVariable.woff2?v=4.0\") format(\"woff2\");\n\t}\n}\n\n@layer base {\n\t* {\n\t\t@apply border-border outline-ring/50;\n\t\toverflow-anchor: none;\n\t}\n\n\tbody {\n\t\t@apply bg-background text-foreground;\n\t\tfont-variant-ligatures: no-contextual;\n\t}\n\n\tbutton {\n\t\tcursor: pointer;\n\t}\n}\n\n@utility container {\n\tmax-width: var(--container);\n\t@apply mx-auto px-4;\n}\n\n@utility link {\n\t@apply text-primary font-medium underline-offset-4 hover:underline;\n}\n\n@utility ns-dialog {\n\t/* New system dialog width */\n\tmin-width: 30.3rem;\n}\n\n.recharts-tooltip-wrapper {\n\tz-index: 51;\n\t@apply tabular-nums;\n}\n\n.recharts-yAxis {\n\t@apply tabular-nums;\n}\n"
  },
  {
    "path": "internal/site/src/lib/alerts.ts",
    "content": "import { t } from \"@lingui/core/macro\"\nimport { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from \"lucide-react\"\nimport type { RecordSubscription } from \"pocketbase\"\nimport { EthernetIcon, GpuIcon } from \"@/components/ui/icons\"\nimport { $alerts } from \"@/lib/stores\"\nimport type { AlertInfo, AlertRecord } from \"@/types\"\nimport { pb } from \"./api\"\nimport { ThermometerIcon, BatteryMediumIcon, HourglassIcon } from \"@/components/ui/icons\"\n\n/** Alert info for each alert type */\nexport const alertInfo: Record<string, AlertInfo> = {\n\tStatus: {\n\t\tname: () => t`Status`,\n\t\tunit: \"\",\n\t\ticon: ServerIcon,\n\t\tdesc: () => t`Triggers when status switches between up and down`,\n\t\t/** \"for x minutes\" is appended to desc when only one value */\n\t\tsingleDesc: () => `${t`System`} ${t`Down`}`,\n\t},\n\tCPU: {\n\t\tname: () => t`CPU Usage`,\n\t\tunit: \"%\",\n\t\ticon: CpuIcon,\n\t\tdesc: () => t`Triggers when CPU usage exceeds a threshold`,\n\t},\n\tMemory: {\n\t\tname: () => t`Memory Usage`,\n\t\tunit: \"%\",\n\t\ticon: MemoryStickIcon,\n\t\tdesc: () => t`Triggers when memory usage exceeds a threshold`,\n\t},\n\tDisk: {\n\t\tname: () => t`Disk Usage`,\n\t\tunit: \"%\",\n\t\ticon: HardDriveIcon,\n\t\tdesc: () => t`Triggers when usage of any disk exceeds a threshold`,\n\t},\n\tBandwidth: {\n\t\tname: () => t`Bandwidth`,\n\t\tunit: \" MB/s\",\n\t\ticon: EthernetIcon,\n\t\tdesc: () => t`Triggers when combined up/down exceeds a threshold`,\n\t\tmax: 250,\n\t},\n\tGPU: {\n\t\tname: () => t`GPU Usage`,\n\t\tunit: \"%\",\n\t\ticon: GpuIcon,\n\t\tdesc: () => t`Triggers when GPU usage exceeds a threshold`,\n\t},\n\tTemperature: {\n\t\tname: () => t`Temperature`,\n\t\tunit: \"°C\",\n\t\ticon: ThermometerIcon,\n\t\tdesc: () => t`Triggers when any sensor exceeds a threshold`,\n\t},\n\tLoadAvg1: {\n\t\tname: () => t`Load Average 1m`,\n\t\tunit: \"\",\n\t\ticon: HourglassIcon,\n\t\tmax: 100,\n\t\tmin: 0.1,\n\t\tstart: 10,\n\t\tstep: 0.1,\n\t\tdesc: () => t`Triggers when 1 minute load average exceeds a threshold`,\n\t},\n\tLoadAvg5: {\n\t\tname: () => t`Load Average 5m`,\n\t\tunit: \"\",\n\t\ticon: HourglassIcon,\n\t\tmax: 100,\n\t\tmin: 0.1,\n\t\tstart: 10,\n\t\tstep: 0.1,\n\t\tdesc: () => t`Triggers when 5 minute load average exceeds a threshold`,\n\t},\n\tLoadAvg15: {\n\t\tname: () => t`Load Average 15m`,\n\t\tunit: \"\",\n\t\ticon: HourglassIcon,\n\t\tmin: 0.1,\n\t\tmax: 100,\n\t\tstart: 10,\n\t\tstep: 0.1,\n\t\tdesc: () => t`Triggers when 15 minute load average exceeds a threshold`,\n\t},\n\tBattery: {\n\t\tname: () => t`Battery`,\n\t\tunit: \"%\",\n\t\ticon: BatteryMediumIcon,\n\t\tdesc: () => t`Triggers when battery charge drops below a threshold`,\n\t\tstart: 20,\n\t\tinvert: true,\n\t},\n} as const\n\n/** Helper to manage user alerts */\nexport const alertManager = (() => {\n\tconst collection = pb.collection<AlertRecord>(\"alerts\")\n\tlet unsub: () => void\n\n\t/** Fields to fetch from alerts collection */\n\tconst fields = \"id,name,system,value,min,triggered\"\n\n\t/** Fetch alerts from collection */\n\tasync function fetchAlerts(): Promise<AlertRecord[]> {\n\t\treturn await collection.getFullList<AlertRecord>({ fields, sort: \"updated\" })\n\t}\n\n\t/** Format alerts into a map of system id to alert name to alert record */\n\tfunction add(alerts: AlertRecord[]) {\n\t\tfor (const alert of alerts) {\n\t\t\tconst systemId = alert.system\n\t\t\tconst systemAlerts = $alerts.get()[systemId] ?? new Map()\n\t\t\tconst newAlerts = new Map(systemAlerts)\n\t\t\tnewAlerts.set(alert.name, alert)\n\t\t\t$alerts.setKey(systemId, newAlerts)\n\t\t}\n\t}\n\n\tfunction remove(alerts: Pick<AlertRecord, \"name\" | \"system\">[]) {\n\t\tfor (const alert of alerts) {\n\t\t\tconst systemId = alert.system\n\t\t\tconst systemAlerts = $alerts.get()[systemId]\n\t\t\tconst newAlerts = new Map(systemAlerts)\n\t\t\tnewAlerts.delete(alert.name)\n\t\t\t$alerts.setKey(systemId, newAlerts)\n\t\t}\n\t}\n\n\tconst actionFns = {\n\t\tcreate: add,\n\t\tupdate: add,\n\t\tdelete: remove,\n\t}\n\n\t// batch alert updates to prevent unnecessary re-renders when adding many alerts at once\n\tconst batchUpdate = (() => {\n\t\tconst batch = new Map<string, RecordSubscription<AlertRecord>>()\n\t\tlet timeout: ReturnType<typeof setTimeout>\n\n\t\treturn (data: RecordSubscription<AlertRecord>) => {\n\t\t\tconst { record } = data\n\t\t\tbatch.set(`${record.system}${record.name}`, data)\n\t\t\tclearTimeout(timeout)\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\tconst groups = { create: [], update: [], delete: [] } as Record<string, AlertRecord[]>\n\t\t\t\tfor (const { action, record } of batch.values()) {\n\t\t\t\t\tgroups[action]?.push(record)\n\t\t\t\t}\n\t\t\t\tfor (const key in groups) {\n\t\t\t\t\tif (groups[key].length) {\n\t\t\t\t\t\tactionFns[key as keyof typeof actionFns]?.(groups[key])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbatch.clear()\n\t\t\t}, 50)\n\t\t}\n\t})()\n\n\tasync function subscribe() {\n\t\tunsub = await collection.subscribe(\"*\", batchUpdate, { fields })\n\t}\n\n\tfunction unsubscribe() {\n\t\tunsub?.()\n\t}\n\n\tasync function refresh() {\n\t\tconst records = await fetchAlerts()\n\t\tadd(records)\n\t}\n\n\treturn {\n\t\t/** Add alerts to store */\n\t\tadd,\n\t\t/** Remove alerts from store */\n\t\tremove,\n\t\t/** Subscribe to alerts */\n\t\tsubscribe,\n\t\t/** Unsubscribe from alerts */\n\t\tunsubscribe,\n\t\t/** Refresh alerts with latest data from hub */\n\t\trefresh,\n\t}\n})()\n"
  },
  {
    "path": "internal/site/src/lib/api.ts",
    "content": "import { t } from \"@lingui/core/macro\"\nimport PocketBase from \"pocketbase\"\nimport { basePath } from \"@/components/router\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport type { ChartTimes, UserSettings } from \"@/types\"\nimport { $alerts, $allSystemsById, $allSystemsByName, $userSettings } from \"./stores\"\nimport { chartTimeData } from \"./utils\"\n\n/** PocketBase JS Client */\nexport const pb = new PocketBase(basePath)\n\nexport const isAdmin = () => pb.authStore.record?.role === \"admin\"\nexport const isReadOnlyUser = () => pb.authStore.record?.role === \"readonly\"\n\nexport const verifyAuth = () => {\n\tpb.collection(\"users\")\n\t\t.authRefresh()\n\t\t.catch(() => {\n\t\t\tlogOut()\n\t\t\ttoast({\n\t\t\t\ttitle: t`Failed to authenticate`,\n\t\t\t\tdescription: t`Please log in again`,\n\t\t\t\tvariant: \"destructive\",\n\t\t\t})\n\t\t})\n}\n\n/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */\nexport function logOut() {\n\t$allSystemsByName.set({})\n\t$allSystemsById.set({})\n\t$alerts.set({})\n\t$userSettings.set({} as UserSettings)\n\tsessionStorage.setItem(\"lo\", \"t\") // prevent auto login on logout\n\tpb.authStore.clear()\n\tpb.realtime.unsubscribe()\n}\n\n/** Fetch or create user settings in database */\nexport async function updateUserSettings() {\n\ttry {\n\t\tconst req = await pb.collection(\"user_settings\").getFirstListItem(\"\", { fields: \"settings\" })\n\t\t$userSettings.set(req.settings)\n\t\treturn\n\t} catch (e) {\n\t\tconsole.error(\"get settings\", e)\n\t}\n\t// create user settings if error fetching existing\n\ttry {\n\t\tconst createdSettings = await pb.collection(\"user_settings\").create({ user: pb.authStore.record?.id })\n\t\t$userSettings.set(createdSettings.settings)\n\t} catch (e) {\n\t\tconsole.error(\"create settings\", e)\n\t}\n}\n\nexport function getPbTimestamp(timeString: ChartTimes, d?: Date) {\n\td ||= chartTimeData[timeString].getOffset(new Date())\n\tconst year = d.getUTCFullYear()\n\tconst month = String(d.getUTCMonth() + 1).padStart(2, \"0\")\n\tconst day = String(d.getUTCDate()).padStart(2, \"0\")\n\tconst hours = String(d.getUTCHours()).padStart(2, \"0\")\n\tconst minutes = String(d.getUTCMinutes()).padStart(2, \"0\")\n\tconst seconds = String(d.getUTCSeconds()).padStart(2, \"0\")\n\n\treturn `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`\n}\n"
  },
  {
    "path": "internal/site/src/lib/enums.ts",
    "content": "/** Operating system */\nexport enum Os {\n\tLinux = 0,\n\tDarwin,\n\tWindows,\n\tFreeBSD,\n}\n\n/** Type of chart */\nexport enum ChartType {\n\tMemory,\n\tDisk,\n\tNetwork,\n\tCPU,\n}\n\n/** Unit of measurement */\nexport enum Unit {\n\tBytes,\n\tBits,\n\tCelsius,\n\tFahrenheit,\n}\n\n/** Meter state for color */\nexport enum MeterState {\n\tGood,\n\tWarn,\n\tCrit,\n}\n\n/** System status states */\nexport enum SystemStatus {\n\tUp = \"up\",\n\tDown = \"down\",\n\tPending = \"pending\",\n\tPaused = \"paused\",\n}\n\n/** Battery state */\nexport enum BatteryState {\n\tUnknown,\n\tEmpty,\n\tFull,\n\tCharging,\n\tDischarging,\n\tIdle,\n}\n\n/** Time format */\nexport enum HourFormat {\n\t// Default = \"Default\",\n\t\"12h\" = \"12h\",\n\t\"24h\" = \"24h\",\n}\n\n/** Container health status */\nexport enum ContainerHealth {\n\tNone,\n\tStarting,\n\tHealthy,\n\tUnhealthy,\n}\n\nexport const ContainerHealthLabels = [\"None\", \"Starting\", \"Healthy\", \"Unhealthy\"] as const\n\n/** Connection type */\nexport enum ConnectionType {\n\tSSH = 1,\n\tWebSocket,\n}\n\nexport const connectionTypeLabels = [\"\", \"SSH\", \"WebSocket\"] as const\n\n/** Systemd service state */\nexport enum ServiceStatus {\n\tActive,\n\tInactive,\n\tFailed,\n\tActivating,\n\tDeactivating,\n\tReloading,\n}\n\nexport const ServiceStatusLabels = [\"Active\", \"Inactive\", \"Failed\", \"Activating\", \"Deactivating\", \"Reloading\"] as const\n\n/** Systemd service sub state */\nexport enum ServiceSubState {\n\tDead,\n\tRunning,\n\tExited,\n\tFailed,\n\tUnknown,\n}\n\nexport const ServiceSubStateLabels = [\"Dead\", \"Running\", \"Exited\", \"Failed\", \"Unknown\"] as const\n"
  },
  {
    "path": "internal/site/src/lib/i18n.ts",
    "content": "import type { Messages } from \"@lingui/core\"\nimport { i18n } from \"@lingui/core\"\nimport { t } from \"@lingui/core/macro\"\nimport { detect, fromNavigator, fromStorage } from \"@lingui/detect-locale\"\nimport languages from \"@/lib/languages\"\nimport { messages as enMessages } from \"@/locales/en/en\"\nimport { BatteryState } from \"./enums\"\nimport { $direction } from \"./stores\"\n\nconst rtlLanguages = new Set([\"ar\", \"fa\", \"he\"])\n\n// activates locale\nfunction activateLocale(locale: string, messages: Messages = enMessages) {\n\ti18n.load(locale, messages)\n\ti18n.activate(locale)\n\tdocument.documentElement.lang = locale\n\tlocalStorage.setItem(\"lang\", locale)\n\t$direction.set(rtlLanguages.has(locale) ? \"rtl\" : \"ltr\")\n}\n\n// dynamically loads translations for the given locale\nexport async function dynamicActivate(locale: string) {\n\tif (locale === \"en\") {\n\t\tactivateLocale(locale)\n\t} else {\n\t\ttry {\n\t\t\tconst { messages }: { messages: Messages } = await import(`../locales/${locale}/${locale}.ts`)\n\t\t\tactivateLocale(locale, messages)\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error loading ${locale}`, error)\n\t\t\tactivateLocale(\"en\")\n\t\t}\n\t}\n}\n\nexport function getLocale() {\n\t// let locale = detect(fromUrl(\"lang\"), fromStorage(\"lang\"), fromNavigator(), \"en\")\n\tlet locale = detect(fromStorage(\"lang\"), fromNavigator(), \"en\")\n\t// log if dev\n\tif (import.meta.env.DEV) {\n\t\tconsole.log(\"detected locale\", locale)\n\t}\n\t// handle zh variants\n\tif (locale?.startsWith(\"zh-\")) {\n\t\t// map zh variants to zh-CN\n\t\tconst zhVariantMap: Record<string, string> = {\n\t\t\t\"zh-HK\": \"zh-HK\",\n\t\t\t\"zh-TW\": \"zh\",\n\t\t\t\"zh-MO\": \"zh\",\n\t\t\t\"zh-Hant\": \"zh\",\n\t\t}\n\t\treturn zhVariantMap[locale] || \"zh-CN\"\n\t}\n\tlocale = (locale || \"en\").split(\"-\")[0]\n\t// use en if locale is not in languages\n\tif (!languages.some((l) => l[0] === locale)) {\n\t\tlocale = \"en\"\n\t}\n\treturn locale\n}\n\n////////////////////////////////////////////////////////\n\nexport const batteryStateTranslations = {\n\t[BatteryState.Unknown]: () => t({ message: \"Unknown\", comment: \"Context: Battery state\" }),\n\t[BatteryState.Empty]: () => t({ message: \"Empty\", comment: \"Context: Battery state\" }),\n\t[BatteryState.Full]: () => t({ message: \"Full\", comment: \"Context: Battery state\" }),\n\t[BatteryState.Charging]: () => t({ message: \"Charging\", comment: \"Context: Battery state\" }),\n\t[BatteryState.Discharging]: () => t({ message: \"Discharging\", comment: \"Context: Battery state\" }),\n\t[BatteryState.Idle]: () => t({ message: \"Idle\", comment: \"Context: Battery state\" }),\n} as const\n"
  },
  {
    "path": "internal/site/src/lib/languages.ts",
    "content": "export default [\n\t[\"ar\", \"العربية\", \"🇵🇸\"],\n\t[\"bg\", \"Български\", \"🇧🇬\"],\n\t[\"cs\", \"Čeština\", \"🇨🇿\"],\n\t[\"da\", \"Dansk\", \"🇩🇰\"],\n\t[\"de\", \"Deutsch\", \"🇩🇪\"],\n\t[\"en\", \"English\", \"🇬🇧\"],\n\t[\"es\", \"Español\", \"🇪🇸\"],\n\t[\"fa\", \"فارسی\", \"🇮🇷\"],\n\t[\"fr\", \"Français\", \"🇫🇷\"],\n\t[\"he\", \"עברית\", \"\"],\n\t[\"hr\", \"Hrvatski\", \"🇭🇷\"],\n\t[\"hu\", \"Magyar\", \"🇭🇺\"],\n\t[\"id\", \"Indonesia\", \"🇮🇩\"],\n\t[\"it\", \"Italiano\", \"🇮🇹\"],\n\t[\"ja\", \"日本語\", \"🇯🇵\"],\n\t[\"ko\", \"한국어\", \"🇰🇷\"],\n\t[\"nl\", \"Nederlands\", \"🇳🇱\"],\n\t[\"no\", \"Norsk\", \"🇳🇴\"],\n\t[\"pl\", \"Polski\", \"🇵🇱\"],\n\t[\"pt\", \"Português\", \"🇵🇹\"],\n\t[\"ru\", \"Русский\", \"🇷🇺\"],\n\t[\"sl\", \"Slovenščina\", \"🇸🇮\"],\n\t[\"sr\", \"Српски\", \"🇷🇸\"],\n\t[\"sv\", \"Svenska\", \"🇸🇪\"],\n\t[\"tr\", \"Türkçe\", \"🇹🇷\"],\n\t[\"uk\", \"Українська\", \"🇺🇦\"],\n\t[\"vi\", \"Tiếng Việt\", \"🇻🇳\"],\n\t[\"zh-CN\", \"简体中文\", \"🇨🇳\"],\n\t[\"zh-HK\", \"繁體中文\", \"🇭🇰\"],\n\t[\"zh\", \"繁體中文\", \"🇹🇼\"],\n] as const\n"
  },
  {
    "path": "internal/site/src/lib/shiki.ts",
    "content": "// https://shiki.style/guide/bundles#fine-grained-bundle\n\n// directly import the theme and language modules, only the ones you imported will be bundled.\nimport githubDarkDimmed from '@shikijs/themes/github-dark-dimmed'\n\n// `shiki/core` entry does not include any themes or languages or the wasm binary.\nimport { createHighlighterCore } from 'shiki/core'\nimport { createOnigurumaEngine } from 'shiki/engine/oniguruma'\n\nexport const highlighter = await createHighlighterCore({\n   themes: [\n      // instead of strings, you need to pass the imported module\n      githubDarkDimmed,\n      // or a dynamic import if you want to do chunk splitting\n      //  import('@shikijs/themes/material-theme-ocean')\n   ],\n   langs: [\n      import('@shikijs/langs/log'),\n      import('@shikijs/langs/json'),\n      // shiki will try to interop the module with the default export\n      // () => import('@shikijs/langs/css'),\n   ],\n   // `shiki/wasm` contains the wasm binary inlined as base64 string.\n   engine: createOnigurumaEngine(import('shiki/wasm'))\n})\n\n// optionally, load themes and languages after creation\n// await highlighter.loadTheme(import('@shikijs/themes/vitesse-light'))\n"
  },
  {
    "path": "internal/site/src/lib/stores.ts",
    "content": "import { atom, computed, listenKeys, map, type ReadableAtom } from \"nanostores\"\nimport type { AlertMap, ChartTimes, SystemRecord, UserSettings } from \"@/types\"\nimport { pb } from \"./api\"\nimport { Unit } from \"./enums\"\n\n/** Default layout width. Used as fallback when user setting is unset. */\nexport const defaultLayoutWidth = 1580\n\n/** Store if user is authenticated */\nexport const $authenticated = atom(pb.authStore.isValid)\n\n/** Map of system records by name */\nexport const $allSystemsByName = map<Record<string, SystemRecord>>({})\n/** Map of system records by id */\nexport const $allSystemsById = map<Record<string, SystemRecord>>({})\n/** Map of up systems by id */\nexport const $upSystems = map<Record<string, SystemRecord>>({})\n/** Map of down systems by id */\nexport const $downSystems = map<Record<string, SystemRecord>>({})\n/** Map of paused systems by id */\nexport const $pausedSystems = map<Record<string, SystemRecord>>({})\n/** List of all system records */\nexport const $systems: ReadableAtom<SystemRecord[]> = computed($allSystemsById, Object.values)\n\n/** Map of alert records by system id and alert name */\nexport const $alerts = map<AlertMap>({})\n\n/** SSH public key */\nexport const $publicKey = atom(\"\")\n\n/** Chart time period */\nexport const $chartTime = atom<ChartTimes>(\"1h\")\n\n/** Whether to display average or max chart values */\nexport const $maxValues = atom(false)\n\n// export const UserSettingsSchema = v.object({\n// \tchartTime: v.picklist([\"1h\", \"12h\", \"24h\", \"1w\", \"30d\"]),\n// \temails: v.optional(v.array(v.pipe(v.string(), v.email())), [pb?.authStore?.record?.email ?? \"\"]),\n// \twebhooks: v.optional(v.array(v.string())),\n// \tcolorWarn: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),\n// \tcolorDanger: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(100))),\n// \tunitTemp: v.optional(v.enum(Unit)),\n// \tunitNet: v.optional(v.enum(Unit)),\n// \tunitDisk: v.optional(v.enum(Unit)),\n// })\n\n/** User settings */\nexport const $userSettings = map<UserSettings>({\n\tchartTime: \"1h\",\n\temails: [pb.authStore.record?.email || \"\"],\n\tunitNet: Unit.Bytes,\n\tunitTemp: Unit.Celsius,\n})\n// update chart time on change\nlistenKeys($userSettings, [\"chartTime\"], ({ chartTime }) => $chartTime.set(chartTime))\n\n/** Container chart filter */\nexport const $containerFilter = atom(\"\")\n\n/** Temperature chart filter */\nexport const $temperatureFilter = atom(\"\")\n\n/** Fallback copy to clipboard dialog content */\nexport const $copyContent = atom(\"\")\n\n/** Direction for localization */\nexport const $direction = atom<\"ltr\" | \"rtl\">(\"ltr\")\n\n/** Longest system name length. Used to set table column width. I know this\n *  is stupid but the table is virtualized and I know this will work.\n */\nexport const $longestSystemNameLen = atom(8)\n"
  },
  {
    "path": "internal/site/src/lib/systemsManager.ts",
    "content": "/** biome-ignore-all lint/suspicious/noAssignInExpressions: it's fine :) */\nimport type { PreinitializedMapStore } from \"nanostores\"\nimport { pb, verifyAuth } from \"@/lib/api\"\nimport {\n\t$allSystemsById,\n\t$allSystemsByName,\n\t$downSystems,\n\t$longestSystemNameLen,\n\t$pausedSystems,\n\t$upSystems,\n} from \"@/lib/stores\"\nimport { getVisualStringWidth, updateFavicon } from \"@/lib/utils\"\nimport type { SystemRecord } from \"@/types\"\nimport { SystemStatus } from \"./enums\"\n\nconst COLLECTION = pb.collection<SystemRecord>(\"systems\")\nconst FIELDS_DEFAULT = \"id,name,host,port,info,status\"\n\n/** Maximum system name length for display purposes */\nconst MAX_SYSTEM_NAME_LENGTH = 22\n\nlet initialized = false\n// biome-ignore lint/suspicious/noConfusingVoidType: typescript rocks\nlet unsub: (() => void) | undefined | void\n\n/** Initialize the systems manager and set up listeners */\nexport function init() {\n\tif (initialized) {\n\t\treturn\n\t}\n\tinitialized = true\n\n\t// sync system stores on change\n\t$allSystemsById.listen((newSystems, oldSystems, changedKey) => {\n\t\tconst oldSystem = oldSystems[changedKey]\n\t\tconst newSystem = newSystems[changedKey]\n\n\t\t// if system is undefined (deleted), remove it from the stores\n\t\tif (oldSystem && !newSystem?.id) {\n\t\t\tremoveFromStore(oldSystem, $upSystems)\n\t\t\tremoveFromStore(oldSystem, $downSystems)\n\t\t\tremoveFromStore(oldSystem, $pausedSystems)\n\t\t\tremoveFromStore(oldSystem, $allSystemsById)\n\t\t}\n\n\t\tif (!newSystem) {\n\t\t\tonSystemsChanged(newSystems, undefined)\n\t\t\treturn\n\t\t}\n\n\t\tconst newStatus = newSystem.status\n\t\tif (newStatus === SystemStatus.Up) {\n\t\t\t$upSystems.setKey(newSystem.id, newSystem)\n\t\t\tremoveFromStore(newSystem, $downSystems)\n\t\t\tremoveFromStore(newSystem, $pausedSystems)\n\t\t} else if (newStatus === SystemStatus.Down) {\n\t\t\t$downSystems.setKey(newSystem.id, newSystem)\n\t\t\tremoveFromStore(newSystem, $upSystems)\n\t\t\tremoveFromStore(newSystem, $pausedSystems)\n\t\t} else if (newStatus === SystemStatus.Paused) {\n\t\t\t$pausedSystems.setKey(newSystem.id, newSystem)\n\t\t\tremoveFromStore(newSystem, $upSystems)\n\t\t\tremoveFromStore(newSystem, $downSystems)\n\t\t} else if (newStatus === SystemStatus.Pending) {\n\t\t\tremoveFromStore(newSystem, $upSystems)\n\t\t\tremoveFromStore(newSystem, $downSystems)\n\t\t\tremoveFromStore(newSystem, $pausedSystems)\n\t\t}\n\n\t\t// run things that need to be done when systems change\n\t\tonSystemsChanged(newSystems, newSystem)\n\t})\n}\n\n/** Update the longest system name length and favicon based on system status */\nfunction onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: SystemRecord | undefined) {\n\tconst downSystemsStore = $downSystems.get()\n\tconst downSystems = Object.values(downSystemsStore)\n\n\t// Update longest system name length\n\tconst longestName = $longestSystemNameLen.get()\n\tconst nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, getVisualStringWidth(changedSystem?.name || \"\"))\n\tif (nameLen > longestName) {\n\t\t$longestSystemNameLen.set(nameLen)\n\t}\n\n\tupdateFavicon(downSystems.length)\n}\n\n/** Fetch systems from collection */\nasync function fetchSystems(): Promise<SystemRecord[]> {\n\ttry {\n\t\treturn await COLLECTION.getFullList({ sort: \"+name\", fields: FIELDS_DEFAULT })\n\t} catch (error) {\n\t\tconsole.error(\"Failed to fetch systems:\", error)\n\t\treturn []\n\t}\n}\n\n/** Makes sure the system has valid info object and throws if not */\nfunction validateSystemInfo(system: SystemRecord) {\n\tif (!(\"cpu\" in system.info)) {\n\t\tthrow new Error(`${system.name} has no CPU info`)\n\t}\n}\n\n/** Add system to both name and ID stores */\nexport function add(system: SystemRecord) {\n\ttry {\n\t\tvalidateSystemInfo(system)\n\t\t$allSystemsByName.setKey(system.name, system)\n\t\t$allSystemsById.setKey(system.id, system)\n\t} catch (error) {\n\t\tconsole.error(error)\n\t}\n}\n\n/** Update system in stores */\nexport function update(system: SystemRecord) {\n\ttry {\n\t\tvalidateSystemInfo(system)\n\t\t// if name changed, make sure old name is removed from the name store\n\t\tconst oldName = $allSystemsById.get()[system.id]?.name\n\t\tif (oldName !== system.name) {\n\t\t\t$allSystemsByName.setKey(oldName, undefined as unknown as SystemRecord)\n\t\t}\n\t\tadd(system)\n\t} catch (error) {\n\t\tconsole.error(error)\n\t}\n}\n\n/** Remove system from stores */\nexport function remove(system: SystemRecord) {\n\tremoveFromStore(system, $allSystemsByName)\n\tremoveFromStore(system, $allSystemsById)\n\tremoveFromStore(system, $upSystems)\n\tremoveFromStore(system, $downSystems)\n\tremoveFromStore(system, $pausedSystems)\n}\n\n/** Remove system from specific store */\nfunction removeFromStore(system: SystemRecord, store: PreinitializedMapStore<Record<string, SystemRecord>>) {\n\tconst key = store === $allSystemsByName ? system.name : system.id\n\tstore.setKey(key, undefined as unknown as SystemRecord)\n}\n\n/** Action functions for subscription */\nconst actionFns: Record<string, (system: SystemRecord) => void> = {\n\tcreate: add,\n\tupdate: update,\n\tdelete: remove,\n}\n\n/** Subscribe to real-time system updates from the collection */\nexport async function subscribe() {\n\ttry {\n\t\tunsub = await COLLECTION.subscribe(\"*\", ({ action, record }) => actionFns[action]?.(record), {\n\t\t\tfields: FIELDS_DEFAULT,\n\t\t})\n\t} catch (error) {\n\t\tconsole.error(\"Failed to subscribe to systems collection:\", error)\n\t}\n}\n\n/** Refresh all systems with latest data from the hub */\nexport async function refresh() {\n\ttry {\n\t\tconst records = await fetchSystems()\n\t\tif (!records.length) {\n\t\t\t// No systems found, verify authentication\n\t\t\tverifyAuth()\n\t\t\treturn\n\t\t}\n\t\tfor (const record of records) {\n\t\t\tadd(record)\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"Failed to refresh systems:\", error)\n\t}\n}\n\n/** Unsubscribe from real-time system updates */\nexport const unsubscribe = () => (unsub = unsub?.())\n"
  },
  {
    "path": "internal/site/src/lib/time.ts",
    "content": ""
  },
  {
    "path": "internal/site/src/lib/use-intersection-observer.ts",
    "content": "import { useEffect, useRef, useState } from \"react\"\n\n// adapted from usehooks-ts/use-intersection-observer\n\n/** The hook internal state. */\ntype State = {\n\t/** A boolean indicating if the element is intersecting. */\n\tisIntersecting: boolean\n\t/** The intersection observer entry. */\n\tentry?: IntersectionObserverEntry\n}\n\n/** Represents the options for configuring the Intersection Observer. */\ntype UseIntersectionObserverOptions = {\n\t/**\n\t * The element that is used as the viewport for checking visibility of the target.\n\t * @default null\n\t */\n\troot?: Element | Document | null\n\t/**\n\t * A margin around the root.\n\t * @default '0%'\n\t */\n\trootMargin?: string\n\t/**\n\t * A threshold indicating the percentage of the target's visibility needed to trigger the callback.\n\t * @default 0\n\t */\n\tthreshold?: number | number[]\n\t/**\n\t * If true, freezes the intersection state once the element becomes visible.\n\t * @default true\n\t */\n\tfreeze?: boolean\n\t/**\n\t * A callback function to be invoked when the intersection state changes.\n\t * @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.\n\t * @param {IntersectionObserverEntry} entry - The intersection observer Entry.\n\t * @default undefined\n\t */\n\tonChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void\n\t/**\n\t * The initial state of the intersection.\n\t * @default false\n\t */\n\tinitialIsIntersecting?: boolean\n}\n\n/**\n * The return type of the useIntersectionObserver hook.\n *\n * Supports both tuple and object destructing.\n * @param {(node: Element | null) => void} ref - The ref callback function.\n * @param {boolean} isIntersecting - A boolean indicating if the element is intersecting.\n * @param {IntersectionObserverEntry | undefined} entry - The intersection observer Entry.\n */\ntype IntersectionReturn = {\n\tref: (node?: Element | null) => void\n\tisIntersecting: boolean\n\tentry?: IntersectionObserverEntry\n}\n\n/**\n * Custom hook that tracks the intersection of a DOM element with its containing element or the viewport using the [`Intersection Observer API`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).\n * @param {UseIntersectionObserverOptions} options - The options for the Intersection Observer.\n * @returns {IntersectionReturn} The ref callback, a boolean indicating if the element is intersecting, and the intersection observer entry.\n * @example\n * ```tsx\n * const { ref, isIntersecting, entry } = useIntersectionObserver({ threshold: 0.5 });\n * ```\n */\nexport function useIntersectionObserver({\n\tthreshold = 0,\n\troot = null,\n\trootMargin = \"0%\",\n\tfreeze = true,\n\tinitialIsIntersecting = false,\n\tonChange,\n}: UseIntersectionObserverOptions = {}): IntersectionReturn {\n\tconst [ref, setRef] = useState<Element | null>(null)\n\n\tconst [state, setState] = useState<State>(() => ({\n\t\tisIntersecting: initialIsIntersecting,\n\t\tentry: undefined,\n\t}))\n\n\tconst callbackRef = useRef<UseIntersectionObserverOptions[\"onChange\"]>(undefined)\n\n\tcallbackRef.current = onChange\n\n\tconst frozen = state.entry?.isIntersecting && freeze\n\n\tuseEffect(() => {\n\t\t// Ensure we have a ref to observe\n\t\tif (!ref) return\n\n\t\t// Ensure the browser supports the Intersection Observer API\n\t\tif (!(\"IntersectionObserver\" in window)) return\n\n\t\t// Skip if frozen\n\t\tif (frozen) return\n\n\t\tlet unobserve: (() => void) | undefined\n\n\t\tconst observer = new IntersectionObserver(\n\t\t\t(entries: IntersectionObserverEntry[]): void => {\n\t\t\t\tconst thresholds = Array.isArray(observer.thresholds) ? observer.thresholds : [observer.thresholds]\n\n\t\t\t\tentries.forEach((entry) => {\n\t\t\t\t\tconst isIntersecting =\n\t\t\t\t\t\tentry.isIntersecting && thresholds.some((threshold) => entry.intersectionRatio >= threshold)\n\n\t\t\t\t\tsetState({ isIntersecting, entry })\n\n\t\t\t\t\tif (callbackRef.current) {\n\t\t\t\t\t\tcallbackRef.current(isIntersecting, entry)\n\t\t\t\t\t}\n\n\t\t\t\t\tif (isIntersecting && freeze && unobserve) {\n\t\t\t\t\t\tunobserve()\n\t\t\t\t\t\tunobserve = undefined\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t},\n\t\t\t{ threshold, root, rootMargin }\n\t\t)\n\n\t\tobserver.observe(ref)\n\n\t\treturn () => {\n\t\t\tobserver.disconnect()\n\t\t}\n\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [\n\t\tref,\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\tJSON.stringify(threshold),\n\t\troot,\n\t\trootMargin,\n\t\tfrozen,\n\t\tfreeze,\n\t])\n\n\t// ensures that if the observed element changes, the intersection observer is reinitialized\n\tconst prevRef = useRef<Element | null>(null)\n\n\tuseEffect(() => {\n\t\tif (!ref && state.entry?.target && !freeze && !frozen && prevRef.current !== state.entry.target) {\n\t\t\tprevRef.current = state.entry.target\n\t\t\tsetState({ isIntersecting: initialIsIntersecting, entry: undefined })\n\t\t}\n\t}, [ref, state.entry, freeze, frozen, initialIsIntersecting])\n\n\treturn {\n\t\tref: setRef,\n\t\tisIntersecting: !!state.isIntersecting,\n\t\tentry: state.entry,\n\t} as IntersectionReturn\n}\n"
  },
  {
    "path": "internal/site/src/lib/utils.ts",
    "content": "import { plural, t } from \"@lingui/core/macro\"\nimport { type ClassValue, clsx } from \"clsx\"\nimport { listenKeys } from \"nanostores\"\nimport { timeDay, timeHour, timeMinute } from \"d3-time\"\nimport { useEffect, useState } from \"react\"\nimport { twMerge } from \"tailwind-merge\"\nimport { toast } from \"@/components/ui/use-toast\"\nimport type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from \"@/types\"\nimport { HourFormat, Unit } from \"./enums\"\nimport { $copyContent, $userSettings } from \"./stores\"\n\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs))\n}\n\n/** Adds event listener to node and returns function that removes the listener */\nexport function listen<T extends Event = Event>(node: Node, event: string, handler: (event: T) => void) {\n\tnode.addEventListener(event, handler as EventListener)\n\treturn () => node.removeEventListener(event, handler as EventListener)\n}\n\nexport async function copyToClipboard(content: string) {\n\tconst duration = 1500\n\ttry {\n\t\tawait navigator.clipboard.writeText(content)\n\t\ttoast({\n\t\t\tduration,\n\t\t\tdescription: t`Copied to clipboard`,\n\t\t})\n\t} catch (_e) {\n\t\t$copyContent.set(content)\n\t}\n}\n\n// Create formatters directly without intermediate containers\nconst createHourWithMinutesFormatter = (hour12?: boolean) =>\n\tnew Intl.DateTimeFormat(undefined, {\n\t\thour: \"numeric\",\n\t\tminute: \"numeric\",\n\t\thour12,\n\t})\n\nconst createShortDateFormatter = (hour12?: boolean) =>\n\tnew Intl.DateTimeFormat(undefined, {\n\t\tday: \"numeric\",\n\t\tmonth: \"short\",\n\t\thour: \"numeric\",\n\t\tminute: \"numeric\",\n\t\thour12,\n\t})\n\nconst createHourWithSecondsFormatter = (hour12?: boolean) =>\n\tnew Intl.DateTimeFormat(undefined, {\n\t\thour: \"numeric\",\n\t\tminute: \"numeric\",\n\t\tsecond: \"numeric\",\n\t\thour12,\n\t})\n\n// Initialize formatters with default values\nlet hourWithMinutesFormatter = createHourWithMinutesFormatter()\nlet shortDateFormatter = createShortDateFormatter()\nlet hourWithSecondsFormatter = createHourWithSecondsFormatter()\n\nexport const currentHour12 = () => shortDateFormatter.resolvedOptions().hour12\n\nexport const hourWithMinutes = (timestamp: string) => {\n\treturn hourWithMinutesFormatter.format(new Date(timestamp))\n}\n\nexport const formatShortDate = (timestamp: string) => {\n\treturn shortDateFormatter.format(new Date(timestamp))\n}\n\nexport const hourWithSeconds = (timestamp: string) => {\n\treturn hourWithSecondsFormatter.format(new Date(timestamp))\n}\n\n// Update the time formatters if user changes hourFormat\nlistenKeys($userSettings, [\"hourFormat\"], ({ hourFormat }) => {\n\tif (!hourFormat) return\n\tconst newHour12 = hourFormat === HourFormat[\"12h\"]\n\tif (currentHour12() !== newHour12) {\n\t\thourWithMinutesFormatter = createHourWithMinutesFormatter(newHour12)\n\t\tshortDateFormatter = createShortDateFormatter(newHour12)\n\t\thourWithSecondsFormatter = createHourWithSecondsFormatter(newHour12)\n\t}\n})\n\nconst dayFormatter = new Intl.DateTimeFormat(undefined, {\n\tday: \"numeric\",\n\tmonth: \"short\",\n})\nexport const formatDay = (timestamp: string) => {\n\treturn dayFormatter.format(new Date(timestamp))\n}\n\nexport const updateFavicon = (() => {\n\tlet prevDownCount = 0\n\treturn (downCount = 0) => {\n\t\tif (downCount === prevDownCount) {\n\t\t\treturn\n\t\t}\n\t\tprevDownCount = downCount\n\t\tconst svg = `\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 56 70\">\n  <defs>\n    <linearGradient id=\"gradient\" x1=\"0%\" y1=\"20%\" x2=\"100%\" y2=\"120%\">\n      <stop offset=\"0%\" style=\"stop-color:#747bff\"/>\n      <stop offset=\"100%\" style=\"stop-color:#24eb5c\"/>\n    </linearGradient>\n  </defs>\n  <path fill=\"url(#gradient)\" d=\"M35 70H0V0h35q4.4 0 8.2 1.7a21.4 21.4 0 0 1 6.6 4.5q2.9 2.8 4.5 6.6Q56 16.7 56 21a15.4 15.4 0 0 1-.3 3.2 17.6 17.6 0 0 1-.2.8 19.4 19.4 0 0 1-1.5 4 17 17 0 0 1-2.4 3.4 13.5 13.5 0 0 1-2.6 2.3 12.5 12.5 0 0 1-.4.3q1.7 1 3 2.5Q53 39.1 54 41a18.3 18.3 0 0 1 1.5 4 17.4 17.4 0 0 1 .5 3 15.3 15.3 0 0 1 0 1q0 4.4-1.7 8.2a21.4 21.4 0 0 1-4.5 6.6q-2.8 2.9-6.6 4.6Q39.4 70 35 70ZM14 14v14h21a7 7 0 0 0 2.3-.3 6.6 6.6 0 0 0 .4-.2Q39 27 40 26a6.9 6.9 0 0 0 1.5-2.2q.5-1.3.5-2.8a7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 17 40 16a7 7 0 0 0-2.3-1.4 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Zm0 28v14h21a7 7 0 0 0 2.3-.4 6.6 6.6 0 0 0 .4-.1Q39 54.9 40 54a7 7 0 0 0 1.5-2.2 6.9 6.9 0 0 0 .5-2.6 7.9 7.9 0 0 0 0-.2 7 7 0 0 0-.4-2.3 6.6 6.6 0 0 0-.1-.4Q40.9 45 40 44a7 7 0 0 0-2.3-1.5 6.9 6.9 0 0 0-2.5-.6 7.9 7.9 0 0 0-.2 0H14Z\"/>\n  ${downCount > 0 &&\n\t\t\t`\n\t\t<circle cx=\"40\" cy=\"50\" r=\"22\" fill=\"#f00\"/>\n  \t<text x=\"40\" y=\"60\" font-size=\"34\" text-anchor=\"middle\" fill=\"#fff\" font-family=\"Arial\" font-weight=\"bold\">${downCount}</text>\n\t`\n\t\t\t}\n</svg>\n\t`\n\t\tconst blob = new Blob([svg], { type: \"image/svg+xml\" })\n\t\tconst url = URL.createObjectURL(blob)\n\t\t\t; (document.querySelector(\"link[rel='icon']\") as HTMLLinkElement).href = url\n\t}\n})()\n\nexport const chartTimeData: ChartTimeData = {\n\t\"1m\": {\n\t\ttype: \"1m\",\n\t\texpectedInterval: 2000, // allow a bit of latency for one second updates (#1247)\n\t\tlabel: () => t`1 minute`,\n\t\tformat: (timestamp: string) => hourWithSeconds(timestamp),\n\t\tticks: 3,\n\t\tgetOffset: (endTime: Date) => timeMinute.offset(endTime, -1),\n\t\tminVersion: \"0.13.0\",\n\t},\n\t\"1h\": {\n\t\ttype: \"1m\",\n\t\texpectedInterval: 60_000,\n\t\tlabel: () => t`1 hour`,\n\t\t// ticks: 12,\n\t\tformat: (timestamp: string) => hourWithMinutes(timestamp),\n\t\tgetOffset: (endTime: Date) => timeHour.offset(endTime, -1),\n\t},\n\t\"12h\": {\n\t\ttype: \"10m\",\n\t\texpectedInterval: 60_000 * 10,\n\t\tlabel: () => t`12 hours`,\n\t\tticks: 12,\n\t\tformat: (timestamp: string) => hourWithMinutes(timestamp),\n\t\tgetOffset: (endTime: Date) => timeHour.offset(endTime, -12),\n\t},\n\t\"24h\": {\n\t\ttype: \"20m\",\n\t\texpectedInterval: 60_000 * 20,\n\t\tlabel: () => t`24 hours`,\n\t\tformat: (timestamp: string) => hourWithMinutes(timestamp),\n\t\tgetOffset: (endTime: Date) => timeHour.offset(endTime, -24),\n\t},\n\t\"1w\": {\n\t\ttype: \"120m\",\n\t\texpectedInterval: 60_000 * 120,\n\t\tlabel: () => t`1 week`,\n\t\tticks: 7,\n\t\tformat: (timestamp: string) => formatDay(timestamp),\n\t\tgetOffset: (endTime: Date) => timeDay.offset(endTime, -7),\n\t},\n\t\"30d\": {\n\t\ttype: \"480m\",\n\t\texpectedInterval: 60_000 * 480,\n\t\tlabel: () => t`30 days`,\n\t\tticks: 30,\n\t\tformat: (timestamp: string) => formatDay(timestamp),\n\t\tgetOffset: (endTime: Date) => timeDay.offset(endTime, -30),\n\t},\n}\n\n/** Format number to x decimal places, without trailing zeros */\nexport function toFixedFloat(num: number, digits: number) {\n\treturn parseFloat((digits === 0 ? Math.ceil(num) : num).toFixed(digits))\n}\n\nconst decimalFormatters: Map<number, Intl.NumberFormat> = new Map()\n/** Format number to x decimal places, maintaining trailing zeros */\nexport function decimalString(num: number, digits = 2) {\n\tif (digits === 0) {\n\t\treturn Math.ceil(num).toString()\n\t}\n\tlet formatter = decimalFormatters.get(digits)\n\tif (!formatter) {\n\t\tformatter = new Intl.NumberFormat(undefined, {\n\t\t\tminimumFractionDigits: digits,\n\t\t\tmaximumFractionDigits: digits,\n\t\t})\n\t\tdecimalFormatters.set(digits, formatter)\n\t}\n\treturn formatter.format(num)\n}\n\n/** Get value from local or session storage */\nfunction getStorageValue(key: string, defaultValue: unknown, storageInterface: Storage = localStorage) {\n\tconst saved = storageInterface?.getItem(key)\n\treturn saved ? JSON.parse(saved) : defaultValue\n}\n\n/** Hook to sync value in local or session storage */\nexport function useBrowserStorage<T>(key: string, defaultValue: T, storageInterface: Storage = localStorage) {\n\tkey = `besz-${key}`\n\tconst [value, setValue] = useState(() => {\n\t\treturn getStorageValue(key, defaultValue, storageInterface)\n\t})\n\tuseEffect(() => {\n\t\tstorageInterface?.setItem(key, JSON.stringify(value))\n\t}, [key, value])\n\n\treturn [value, setValue]\n}\n\n/** Format temperature to user's preferred unit */\nexport function formatTemperature(celsius: number, unit?: Unit): { value: number; unit: string } {\n\tif (!unit) {\n\t\tunit = $userSettings.get().unitTemp || Unit.Celsius\n\t}\n\t// biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings\n\tif (unit == Unit.Fahrenheit) {\n\t\treturn {\n\t\t\tvalue: celsius * 1.8 + 32,\n\t\t\tunit: \"°F\",\n\t\t}\n\t}\n\treturn {\n\t\tvalue: celsius,\n\t\tunit: \"°C\",\n\t}\n}\n\n/** Format bytes to user's preferred unit */\nexport function formatBytes(\n\tsize: number,\n\tperSecond = false,\n\tunit = Unit.Bytes,\n\tisMegabytes = false\n): { value: number; unit: string } {\n\t// Convert MB to bytes if isMegabytes is true\n\tif (isMegabytes) size *= 1024 * 1024\n\n\t// biome-ignore lint/suspicious/noDoubleEquals: need loose equality check due to form data being strings\n\tif (unit == Unit.Bits) {\n\t\tconst bits = size * 8\n\t\tconst suffix = perSecond ? \"ps\" : \"\"\n\t\tif (bits < 1000) return { value: bits, unit: `b${suffix}` }\n\t\tif (bits < 1_000_000) return { value: bits / 1_000, unit: `Kb${suffix}` }\n\t\tif (bits < 1_000_000_000)\n\t\t\treturn {\n\t\t\t\tvalue: bits / 1_000_000,\n\t\t\t\tunit: `Mb${suffix}`,\n\t\t\t}\n\t\tif (bits < 1_000_000_000_000)\n\t\t\treturn {\n\t\t\t\tvalue: bits / 1_000_000_000,\n\t\t\t\tunit: `Gb${suffix}`,\n\t\t\t}\n\t\treturn {\n\t\t\tvalue: bits / 1_000_000_000_000,\n\t\t\tunit: `Tb${suffix}`,\n\t\t}\n\t}\n\t// bytes\n\tconst suffix = perSecond ? \"/s\" : \"\"\n\tif (size < 100) return { value: size, unit: `B${suffix}` }\n\tif (size < 1000 * 1024) return { value: size / 1024, unit: `KB${suffix}` }\n\tif (size < 1000 * 1024 ** 2)\n\t\treturn {\n\t\t\tvalue: size / 1024 ** 2,\n\t\t\tunit: `MB${suffix}`,\n\t\t}\n\tif (size < 1000 * 1024 ** 3)\n\t\treturn {\n\t\t\tvalue: size / 1024 ** 3,\n\t\t\tunit: `GB${suffix}`,\n\t\t}\n\treturn {\n\t\tvalue: size / 1024 ** 4,\n\t\tunit: `TB${suffix}`,\n\t}\n}\n\nexport const chartMargin = { top: 12, right: 5 }\n\n/**\n * Retuns value of system host, truncating full path if socket.\n * @example\n * // Assuming system.host is \"/var/run/beszel.sock\"\n * const hostname = getHostDisplayValue(system) // hostname will be \"beszel.sock\"\n */\nexport const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf(\"/\") + 1)\n\n// export function formatUptimeString(uptimeSeconds: number): string {\n// \tif (!uptimeSeconds || isNaN(uptimeSeconds)) return \"\"\n// \tif (uptimeSeconds < 3600) {\n// \t\tconst minutes = Math.trunc(uptimeSeconds / 60)\n// \t\treturn plural({ minutes }, { one: \"# minute\", other: \"# minutes\" })\n// \t} else if (uptimeSeconds < 172800) {\n// \t\tconst hours = Math.trunc(uptimeSeconds / 3600)\n// \t\tconsole.log(hours)\n// \t\treturn plural({ hours }, { one: \"# hour\", other: \"# hours\" })\n// \t} else {\n// \t\tconst days = Math.trunc(uptimeSeconds / 86400)\n// \t\treturn plural({ days }, { one: \"# day\", other: \"# days\" })\n// \t}\n// }\n\n/** Generate a random token for the agent */\nexport const generateToken = () => {\n\ttry {\n\t\treturn crypto?.randomUUID()\n\t} catch (_e) {\n\t\treturn Array.from({ length: 2 }, () => (performance.now() * Math.random()).toString(16).replace(\".\", \"-\")).join(\"-\")\n\t}\n}\n\n/** Get the hub URL from the global BESZEL object */\nexport const getHubURL = () => globalThis.BESZEL?.HUB_URL || window.location.origin\n\n/** Map of system IDs to their corresponding tokens (used to avoid fetching in add-system dialog) */\nexport const tokenMap = new Map<SystemRecord[\"id\"], FingerprintRecord[\"token\"]>()\n\n/** Calculate duration between two dates and format as human-readable string */\nexport function formatDuration(\n\tcreatedDate: string | null | undefined,\n\tresolvedDate: string | null | undefined\n): string {\n\tconst created = createdDate ? new Date(createdDate) : null\n\tconst resolved = resolvedDate ? new Date(resolvedDate) : null\n\n\tif (!created || !resolved) return \"\"\n\n\tconst diffMs = resolved.getTime() - created.getTime()\n\tif (diffMs < 0) return \"\"\n\n\tconst totalSeconds = Math.floor(diffMs / 1000)\n\tlet hours = Math.floor(totalSeconds / 3600)\n\tlet minutes = Math.floor((totalSeconds % 3600) / 60)\n\tlet seconds = totalSeconds % 60\n\n\t// if seconds are close to 60, round up to next minute\n\t// if minutes are close to 60, round up to next hour\n\tif (seconds >= 58) {\n\t\tminutes += 1\n\t\tseconds = 0\n\t}\n\tif (minutes >= 60) {\n\t\thours += 1\n\t\tminutes = 0\n\t}\n\n\t// For durations over 1 hour, omit seconds for cleaner display\n\tif (hours > 0) {\n\t\treturn [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null].filter(Boolean).join(\" \")\n\t}\n\n\treturn [hours ? `${hours}h` : null, minutes ? `${minutes}m` : null, seconds ? `${seconds}s` : null]\n\t\t.filter(Boolean)\n\t\t.join(\" \")\n}\n\n/** Parse semver string into major, minor, and patch numbers \n * @example\n * const semVer = \"1.2.3\"\n * const { major, minor, patch } = parseSemVer(semVer)\n * console.log(major, minor, patch) // 1, 2, 3\n*/\nexport const parseSemVer = (semVer = \"\"): SemVer => {\n\t// if (semVer.startsWith(\"v\")) {\n\t// \tsemVer = semVer.slice(1)\n\t// }\n\tif (semVer.includes(\"-\")) {\n\t\tsemVer = semVer.slice(0, semVer.indexOf(\"-\"))\n\t}\n\tconst parts = semVer.split(\".\").map(Number)\n\treturn { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 }\n}\n\n/** Compare two semver strings. Returns -1 if a is less than b, 0 if a is equal to b, and 1 if a is greater than b. */\nexport function compareSemVer(a: SemVer, b: SemVer) {\n\tif (a.major !== b.major) {\n\t\treturn a.major - b.major\n\t}\n\tif (a.minor !== b.minor) {\n\t\treturn a.minor - b.minor\n\t}\n\treturn a.patch - b.patch\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: any is used to allow any function to be passed in\nexport function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {\n\tlet timeout: ReturnType<typeof setTimeout>\n\treturn (...args: Parameters<T>) => {\n\t\tclearTimeout(timeout)\n\t\ttimeout = setTimeout(() => func(...args), wait)\n\t}\n}\n\n// Cache for runOnce\n// biome-ignore lint/complexity/noBannedTypes: Function is used to allow any function to be passed in\nconst runOnceCache = new WeakMap<Function, { done: boolean; result: unknown }>()\n/** Run a function only once */\n// biome-ignore lint/suspicious/noExplicitAny: any is used to allow any function to be passed in\nexport function runOnce<T extends (...args: any[]) => any>(fn: T): T {\n\treturn ((...args: Parameters<T>) => {\n\t\tlet state = runOnceCache.get(fn)\n\t\tif (!state) {\n\t\t\tstate = { done: false, result: undefined }\n\t\t\trunOnceCache.set(fn, state)\n\t\t}\n\t\tif (!state.done) {\n\t\t\tstate.result = fn(...args)\n\t\t\tstate.done = true\n\t\t}\n\t\treturn state.result\n\t}) as T\n}\n\n/** Get the visual width of a string, accounting for full-width characters */\nexport function getVisualStringWidth(str: string): number {\n\tlet width = 0\n\tfor (const char of str) {\n\t\tconst code = char.codePointAt(0) || 0\n\t\t// Hangul Jamo and Syllables are often slightly thinner than Hanzi/Kanji\n\t\tif ((code >= 0x1100 && code <= 0x115f) || (code >= 0xac00 && code <= 0xd7af)) {\n\t\t\twidth += 1.8\n\t\t\tcontinue\n\t\t}\n\t\t// Count CJK and other full-width characters as 2 units, others as 1\n\t\t// Arabic and Cyrillic are counted as 1\n\t\tconst isFullWidth =\n\t\t\t(code >= 0x2e80 && code <= 0x9fff) || // CJK Radicals, Symbols, and Ideographs\n\t\t\t(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs\n\t\t\t(code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms\n\t\t\t(code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms\n\t\t\t(code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols\n\t\t\tcode > 0xffff // Emojis and other supplementary plane characters\n\t\twidth += isFullWidth ? 2 : 1\n\t}\n\treturn width\n}\n\n/** Format seconds to hours, minutes, or seconds */\nexport function secondsToString(seconds: number, unit: \"hour\" | \"minute\" | \"day\"): string {\n\tconst count = Math.floor(seconds / (unit === \"hour\" ? 3600 : unit === \"minute\" ? 60 : 86400))\n\tconst countString = count.toLocaleString()\n\tswitch (unit) {\n\t\tcase \"minute\":\n\t\t\treturn plural(count, { one: `${countString} minute`, few: `${countString} minutes`, many: `${countString} minutes`, other: `${countString} minutes` })\n\t\tcase \"hour\":\n\t\t\treturn plural(count, { one: `${countString} hour`, other: `${countString} hours` })\n\t\tcase \"day\":\n\t\t\treturn plural(count, { one: `${countString} day`, other: `${countString} days` })\n\t}\n}\n\n/** Format seconds to uptime string - \"X minutes\", \"X hours\", \"X days\" */\nexport function secondsToUptimeString(seconds: number): string {\n\tif (seconds < 3600) {\n\t\treturn secondsToString(seconds, \"minute\")\n\t} else if (seconds < 360000) {\n\t\treturn secondsToString(seconds, \"hour\")\n\t} else {\n\t\treturn secondsToString(seconds, \"day\")\n\t}\n}"
  },
  {
    "path": "internal/site/src/locales/ar/ar.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ar\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Arabic\\n\"\n\"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: ar\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"تم تحديد {0} من {1} صف\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# نواة} other {# نواة}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} يوم} other {{countString} أيام}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} ساعة} other {{countString} ساعات}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} دقيقة} few {{countString} دقائق} many {{countString} دقيقة} other {{countString} دقيقة}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# خيط} other {# خيط}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 ساعة\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"دقيقة واحدة\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 دقيقة\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 أسبوع\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 ساعة\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 دقيقة\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 ساعة\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 يومًا\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 دقائق\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"إجراءات\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"نشط\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"التنبيهات النشطة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"الحالة النشطة\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"إضافة {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"إضافة <0>نظام</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"إضافة نظام\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"إضافة رابط\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"تعديل خيارات العرض للرسوم البيانية.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"تعديل عرض التخطيط الرئيسي\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"مسؤول\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"بعد\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"بعد تعيين متغيرات البيئة، أعد تشغيل مركز Beszel الخاص بك لتصبح التغييرات سارية المفعول.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"وكيل\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"سجل التنبيهات\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"التنبيهات\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"جميع الحاويات\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"جميع الأنظمة\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"هل أنت متأكد أنك تريد حذف {name}؟\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"هل أنت متأكد؟\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"النسخ التلقائي يتطلب سياقًا آمنًا.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"متوسط\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"متوسط استخدام وحدة المعالجة المركزية للحاويات\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"المتوسط ينخفض أقل من <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"المتوسط يتجاوز <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"متوسط ​​استهلاك طاقة وحدة معالجة الرسوميات\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"متوسط استخدام وحدة المعالجة المركزية على مستوى النظام\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"متوسط ​​استخدام {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"متوسط استغلال محركات GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"النسخ الاحتياطية\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"عرض النطاق الترددي\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"بطارية\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"البطارية\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"أصبح نشطًا\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"أصبح غير نشط\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"قبل\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"أقل من {0}{1} في آخر {2, plural, one {# دقيقة} other {# دقائق}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"يدعم بيزيل بروتوكول OpenID Connect والعديد من مزوّدي المصادقة عبر بروتوكول OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"يستخدم بيزيل <0>Shoutrrr</0> للتكامل مع خدمات الإشعارات الشهيرة.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"ثنائي\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"بت (كيلوبت/ثانية، ميجابت/ثانية، جيجابت/ثانية)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"حالة التمهيد\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"بايت (كيلوبايت/ثانية، ميجابايت/ثانية، جيجابايت/ثانية)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"ذاكرة التخزين المؤقت / المخازن المؤقتة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"يمكن إعادة التحميل\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"يمكن البدء\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"يمكن الإيقاف\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"إلغاء\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"القدرات\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"السعة\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"تحذير - فقدان محتمل للبيانات\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"درجة مئوية (°م)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"تغيير وحدات عرض المقاييس.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"تغيير خيارات التطبيق العامة.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"الشحن\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"قيد الشحن\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"خيارات الرسم البياني\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"تحقق من {email} للحصول على رابط إعادة التعيين.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"تحقق من السجلات لمزيد من التفاصيل.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"تحقق من خدمة المراقبة الخاصة بك\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"تحقق من خدمة الإشعارات الخاصة بك\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"مسح\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"انقر على حاوية لعرض مزيد من المعلومات.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"انقر على جهاز لعرض مزيد من المعلومات.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"انقر على نظام لعرض مزيد من المعلومات.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"انقر للنسخ\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"تعليمات سطر الأوامر\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"هيئ التنبيهات الواردة\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"تأكيد كلمة المرور\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"التعارضات\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"الاتصال مقطوع\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"متابعة\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"تم النسخ إلى الحافظة\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"نسخ أمر تركيب الدوكر\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"نسخ أمر تشغيل الدوكر\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"نسخ متغيرات البيئة\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"نسخ المضيف\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"نسخ أمر لينكس\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"نسخ الاسم\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"نسخ النص\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"انسخ أمر التثبيت للوكيل أدناه، أو سجل الوكلاء تلقائياً باستخدام <0>رمز مميز عالمي</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"انسخ محتوى <0>docker-compose.yml</0> للوكيل أدناه، أو سجل الوكلاء تلقائياً باستخدام <1>رمز مميز عالمي</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"نسخ YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"المعالج\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"نوى المعالج\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"ذروة المعالج\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"وقت المعالج\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"تفصيل وقت المعالج\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"استخدام وحدة المعالجة المركزية\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"إنشاء\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"إنشاء حساب\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"أنشئت\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"حرج (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"التنزيل التراكمي\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"الرفع التراكمي\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"الحالة الحالية\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"الدورات\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"يوميًا\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"الفترة الزمنية الافتراضية\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"حذف\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"حذف البصمة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"الوصف\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"التفاصيل\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"الجهاز\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"قيد التفريغ\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"القرص\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"إدخال/إخراج القرص\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"وحدة القرص\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"استخدام القرص\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"استخدام القرص لـ {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"استخدام المعالج للدوكر\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"استخدام الذاكرة للدوكر\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"إدخال/إخراج الشبكة للدوكر\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"التوثيق\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"معطل\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"معطل ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"تنزيل\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"المدة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"تعديل\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"إضافة {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"البريد الإشباكي\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"إشعارات البريد الإشباكي\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"فارغة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"وقت النهاية\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"عنوان URL للنقطة النهائية\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"عنوان URL للنقطة النهائية لل ping (مطلوب)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"أدخل عنوان البريد الإشباكي لإعادة تعيين كلمة المرور\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"أدخل عنوان البريد الإشباكي...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"أدخل كلمة المرور لمرة واحدة الخاصة بك.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"مؤقت\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"خطأ\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"مثال:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"يتجاوز {0}{1} في آخر {2, plural, one {# دقيقة} other {# دقائق}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"معرف العملية الرئيسي للتنفيذ\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"سيتم حذف الأنظمة الحالية غير المعرفة في <0>config.yml</0>. يرجى عمل نسخ احتياطية بانتظام.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"خرج نشطًا\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"ينتهي بعد ساعة واحدة أو عند إعادة تشغيل المحور.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"تصدير\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"تصدير التكوين\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"تصدير تكوين الأنظمة الحالية الخاصة بك.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"فهرنهايت (°ف)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"فشل\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"السمات الفاشلة:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"فشل في المصادقة\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"فشل في حفظ الإعدادات\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"فشل في إرسال نبضة القلب\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"فشل في إرسال إشعار الاختبار\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"فشل في تحديث التنبيه\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"فشل: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"تصفية...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"البصمة\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"البرمجيات الثابتة\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"لمدة <0>{min}</0> {min, plural, one {دقيقة} other {دقائق}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"هل نسيت كلمة المرور؟\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"أمر FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"ممتلئة\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"عام\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"عالمي\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"محركات GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"استهلاك طاقة وحدة معالجة الرسوميات\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"استخدام وحدة معالجة الرسوميات\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"شبكة\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"الصحة\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"نبضة القلب\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"مراقبة نبضة القلب\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"تم إرسال نبضة القلب بنجاح\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"أمر Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"مضيف / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"طريقة HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"طريقة HTTP: POST، GET، أو HEAD (الافتراضي: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"خاملة\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"إذا فقدت كلمة المرور لحساب المسؤول الخاص بك، يمكنك إعادة تعيينها باستخدام الأمر التالي.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"صورة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"غير نشط\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"الفاصل الزمني\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"عنوان البريد الإشباكي غير صالح.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"اللغة\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"التخطيط\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"عرض التخطيط\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"دورة الحياة\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"الحد\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"متوسط التحميل\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"متوسط التحميل 15 دقيقة\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"متوسط التحميل 1 دقيقة\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"متوسط التحميل 5 دقائق\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"متوسط التحميل\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"حالة التحميل\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"جاري التحميل...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"تسجيل الخروج\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"تسجيل الدخول\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"فشل محاولة تسجيل الدخول\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"السجلات\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"هل تبحث عن مكان لإنشاء التنبيهات؟ انقر على أيقونات الجرس <0/> في جدول الأنظمة.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"معرف العملية الرئيسي\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"إدارة تفضيلات العرض والإشعارات.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"تعليمات الإعداد اليدوي\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"الحد الأقصى دقيقة\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"الذاكرة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"حد الذاكرة\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"ذروة الذاكرة\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"استخدام الذاكرة\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"استخدام الذاكرة لحاويات دوكر\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"الموديل\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"الاسم\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"الشبكة\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"حركة مرور الشبكة لحاويات الدوكر\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"حركة مرور الشبكة للواجهات العامة\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"وحدة الشبكة\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"لا\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"لم يتم العثور على نتائج.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"لا توجد نتائج.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"لا توجد سمات S.M.A.R.T. متاحة لهذا الجهاز.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"لم يتم العثور على أنظمة.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"الإشعارات\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"دعم OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"في كل إعادة تشغيل، سيتم تحديث الأنظمة في قاعدة البيانات لتتطابق مع الأنظمة المعرفة في الملف.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"مرة واحدة\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"كلمة مرور لمرة واحدة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"فتح القائمة\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"أو المتابعة باستخدام\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"أخرى\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"الكتابة فوق التنبيهات الحالية\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"صفحة\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"صفحة {0} من {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"الصفحات / الإعدادات\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"كلمة المرور\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"كلمة المرور يجب أن تتكون من 8 أحرف على الأقل.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"يجب أن تكون كلمة المرور أقل من 72 بايت.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"تم استلام طلب إعادة تعيين كلمة المرور\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"الماضي\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"إيقاف مؤقت\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"متوقف مؤقتا\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"متوقف مؤقتا ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"تنسيق الحمولة\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"متوسط الاستخدام لكل نواة\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"النسبة المئوية للوقت المقضي في كل حالة\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"دائم\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"الاستمرارية\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"يرجى <0>تكوين خادم SMTP</0> لضمان تسليم التنبيهات.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"يرجى التحقق من السجلات لمزيد من التفاصيل.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"يرجى التحقق من بيانات الاعتماد الخاصة بك والمحاولة مرة أخرى\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"يرجى إنشاء حساب مسؤول\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"يرجى تمكين النوافذ المنبثقة لهذا الموقع\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"يرجى تسجيل الدخول مرة أخرى\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"يرجى الاطلاع على <0>التوثيق</0> للحصول على التعليمات.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"يرجى تسجيل الدخول إلى حسابك\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"المنفذ\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"تشغيل الطاقة\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"الاستخدام الدقيق في الوقت المسجل\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"اللغة المفضلة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"تم بدء العملية\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"المفتاح العام\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"ساعات الهدوء\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"قراءة\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"تم الاستلام\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"تحديث\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"العلاقات\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"طلب كلمة مرور لمرة واحدة\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"طلب OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"مطلوب من قبل\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"يتطلب\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"إعادة تعيين كلمة المرور\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"تم حلها\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"إعادة التشغيل\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"استئناف\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"الجذر\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"تدوير الرمز المميز\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"صفوف لكل صفحة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"مقاييس وقت التشغيل\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"تفاصيل S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"اختبار S.M.A.R.T. الذاتي\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"احفظ العنوان باستخدام مفتاح الإدخال أو الفاصلة. اتركه فارغًا لتعطيل إشعارات البريد الإشباكي.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"حفظ الإعدادات\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"احفظ النظام\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"محفوظ في قاعدة البيانات ولا ينتهي حتى تقوم بتعطيله.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"جدولة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"جدولة ساعات الهدوء حيث لن يتم إرسال الإشعارات، مثل أثناء فترات الصيانة.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"جدولة ساعات الهدوء حيث لن يتم إرسال الإشعارات.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"بحث\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"البحث عن الأنظمة أو الإعدادات...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"ثواني بين ال pings (الافتراضي: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"راجع <0>إعدادات الإشعارات</0> لتكوين كيفية تلقي التنبيهات.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"تحديد {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"أرسل ping نبضة قلب واحدة للتحقق من أن نقطة النهاية الخاصة بك تعمل.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"أرسل pings صادرة دورية إلى خدمة مراقبة خارجية حتى تتمكن من مراقبة Beszel دون تعريضه للإنترنت.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"إرسال نبضة قلب اختبارية\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"تم الإرسال\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"الرقم التسلسلي\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"تفاصيل الخدمة\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"الخدمات\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"تعيين عتبات النسبة المئوية لألوان العداد.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"قم بتعيين متغيرات البيئة التالية على مركز Beszel الخاص بك لتمكين مراقبة نبضة القلب:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"الإعدادات\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"تم حفظ الإعدادات\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"تسجيل الدخول\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"إعدادات SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"الترتيب حسب\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"وقت البدء\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"الحالة\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"الحالة\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"الحالة الفرعية\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"مساحة التبديل المستخدمة من قبل النظام\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"استخدام التبديل\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"النظام\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"متوسط تحميل النظام مع مرور الوقت\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"خدمات systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"الأنظمة\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"يمكن إدارة الأنظمة في ملف <0>config.yml</0> داخل دليل البيانات الخاص بك.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"جدول\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"المهام\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"درجة الحرارة\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"درجة الحرارة\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"وحدة درجة الحرارة\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"درجات حرارة مستشعرات النظام\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"اختبار <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"اختبار نبضة القلب\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"تم إرسال إشعار الاختبار\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"الحالة العامة هي <0>موافق</0> عندما تكون جميع الأنظمة تعمل، و<1>تحذير</1> عند تشغيل التنبيهات، و<2>خطأ</2> عندما يكون أي نظام معطلاً.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"ثم قم بتسجيل الدخول إلى الواجهة الخلفية وأعد تعيين كلمة مرور حساب المستخدم الخاص بك في جدول المستخدمين.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"لا يمكن التراجع عن هذا الإجراء. سيؤدي ذلك إلى حذف جميع السجلات الحالية لـ {name} من قاعدة البيانات بشكل دائم.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"سيؤدي هذا إلى حذف جميع السجلات المحددة من قاعدة البيانات بشكل دائم.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"معدل نقل {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"معدل نقل نظام الملفات الجذر\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"تنسيق الوقت\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"إلى البريد الإشباكي\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"تبديل الشبكة\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"تبديل السمة\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"رمز مميز\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"الرموز المميزة والبصمات\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"تسمح الرموز المميزة للوكلاء بالاتصال والتسجيل. البصمات هي معرفات مستقرة فريدة لكل نظام، يتم تعيينها عند الاتصال الأول.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"تُستخدم الرموز المميزة والبصمات للمصادقة على اتصالات WebSocket إلى المحور.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"الإجمالي\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"إجمالي البيانات المستلمة لكل واجهة\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"إجمالي البيانات المرسلة لكل واجهة\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"الإجمالي: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"تم التفعيل بواسطة\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"المحفزات\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز متوسط التحميل لمدة دقيقة واحدة عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 15 دقيقة عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 5 دقائق عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"يتم التفعيل عندما تنخفض شحنة البطارية أقل من عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز الجمع بين الصعود/الهبوط عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز استخدام وحدة المعالجة المركزية عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز استخدام وحدة معالجة الرسوميات عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز استخدام الذاكرة عتبة معينة\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"يتم التفعيل عندما يتغير الحالة بين التشغيل والإيقاف\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"النوع\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"ملف الوحدة\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"تفضيلات الوحدة\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"رمز مميز عالمي\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"غير معروفة\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"غير محدود\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"قيد التشغيل\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"قيد التشغيل ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"تحديث\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"تم التحديث\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"يتم التحديث كل 10 دقائق.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"رفع\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"مدة التشغيل\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"الاستخدام\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"استخدام القسم الجذر\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"مستخدم\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"المستخدمون\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"القيمة\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"عرض\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"عرض المزيد\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"عرض أحدث 200 تنبيه.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"الأعمدة الظاهرة\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"في انتظار وجود سجلات كافية للعرض\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"هل تريد مساعدتنا في تحسين ترجماتنا؟ تحقق من <0>Crowdin</0> لمزيد من التفاصيل.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"يريد\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"تحذير (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"عتبات التحذير\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"إشعارات Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"عند التفعيل، يسمح هذا الرمز المميز للوكلاء بالتسجيل الذاتي دون إنشاء نظام مسبق.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"عند استخدام POST، تتضمن كل نبضة قلب حمولة JSON مع ملخص حالة النظام وقائمة الأنظمة المعطلة والتنبيهات التي تم تشغيلها.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"أمر ويندوز\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"كتابة\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"تكوين YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"تكوين YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"نعم\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"تم تحديث إعدادات المستخدم الخاصة بك.\"\n"
  },
  {
    "path": "internal/site/src/locales/bg/bg.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: bg\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Bulgarian\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: bg\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} от {1} селектирани.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# ядро} other {# ядра}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} ден} other {{countString} дни}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} час} other {{countString} часа}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} минута} few {{countString} минути} many {{countString} минути} other {{countString} минути}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# нишка} other {# нишки}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 час\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 минута\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 минута\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 седмица\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 часа\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 минути\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 часа\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 дни\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 минути\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Действия\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Активен\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Активни тревоги\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Активно състояние\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Добави {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Добави <0>Система</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Добави система\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Добави URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Настрой опциите за показване на диаграмите.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Настройка ширината на основния макет\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Администратор\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"След\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"След като настроите променливите на средата, рестартирайте вашия Beszel hub, за да влязат промените в сила.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Агент\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"История на нотификациите\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Тревоги\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Всички контейнери\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Всички системи\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Сигурен ли си, че искаш да изтриеш {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Сигурни ли сте?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Автоматичното копиране изисква защитен контескт.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Средно\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Средно използване на процесора на контейнерите\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Средната стойност пада под <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Средната стойност надхвърля <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Средна консумация на ток от графични карти\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Средно използване на процесора на цялата система\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Средно използване на {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Средно използване на GPU двигатели\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Архиви\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandwidth на мрежата\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Бат\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Батерия\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Стана активен\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Стана неактивен\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Преди\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Под {0}{1} в последните {2, plural, one {# минута} other {# минути}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel поддържа OpenID Connect и много други OAuth2 доставчици за удостоверяване.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel ползва <0>Shoutrrr</0> за да се интегрира с известни услуги за уведомяване.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Двоичен код\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Бита (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Състояние при зареждане\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Байта (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Кеш / Буфери\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Може да се презареди\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Може да се стартира\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Може да се спре\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Откажи\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Възможности\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Капацитет\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Внимание - възможност за загуба на данни\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Целзий (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Промяна на единиците за показване на метриките.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Смени общите опции на приложението.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Заряд\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Зареждане\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Опции на диаграмата\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Провери {email} за линк за нулиране.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Провери log-овете за повече информация.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Проверете вашата услуга за мониторинг\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Провери услугата си за удостоверяване\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Изчисти\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Кликнете върху контейнер, за да видите повече информация.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Кликнете върху устройство, за да видите повече информация.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Кликнете върху система, за да видите повече информация.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Настисни за да копираш\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Инструкции за командната линия\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Настрой как получаваш нотификации за тревоги.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Потвърди парола\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Конфликти\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Връзката е прекъсната\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Продължи\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Записано в клипборда\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Копирай docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Копирай docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Копирай еnv\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Копирай хоста\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Копирай linux командата\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Копирай име\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Копирай текста\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Копирайте командата за инсталиране на агента по-долу или регистрирайте агентите автоматично с <0>универсален токен</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Копирайте съдържанието на<0>docker-compose.yml</0> за агента по-долу или регистрирайте агентите автоматично с <1>универсален токен</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Копирай YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"Процесор\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU ядра\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Пик на CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Време на CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Разбивка на времето на CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Употреба на процесор\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Създай\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Създай акаунт\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Създаден\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Критично (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Кумулативно изтегляне\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Кумулативно качване\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Текущо състояние\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Цикли\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Дневно\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Времеви диапазон по подразбиране\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Изтрий\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Изтрий пръстов отпечатък\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Описание\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Подробности\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Устройство\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Разреждане\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Диск\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Диск I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Единица за диск\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Използване на диск\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Изполване на диск от {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Използване на процесор от docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Изполване на памет от docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Мрежов I/O използван от docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Документация\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Офлайн\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Офлайн ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Изтегляне\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Продължителност\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Редактирай\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Редактиране на {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Имейл\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Имейл нотификации\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Празна\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Крайно време\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL адрес на крайната точка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL адрес на крайната точка за пинг (задължително)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Въведи имейл адрес за да нулираш паролата\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Въведи имейл адрес...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Въведете Вашата еднократна парола.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Ефимерен\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Грешка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Пример:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Надвишава {0}{1} в последните {2, plural, one {# минута} other {# минути}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID на главния изпълнителен процес\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Съществуващи системи които не са дефинирани в <0>config.yml</0> ще бъдат изтрити. Моля прави чести архиви.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Излезе активно\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Изтича след един час или при рестартиране на хъба.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Експортиране\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Експортирай конфигурация\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Експортирай конфигурацията на системите.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Фаренхайт (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Неуспешно\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Неуспешни атрибути:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Неуспешно удостоверяване\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Неуспешно запазване на настройки\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Неуспешно изпращане на heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Неуспешно изпрати тестова нотификация\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Неуспешно обнови тревога\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Неуспешни: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Филтрирай...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Пръстов отпечатък\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Фърмуер\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"За <0>{min}</0> {min, plural, one {минута} other {минути}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Забравена парола?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD команда\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Пълна\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Общо\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Глобален\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU двигатели\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Консумация на ток от графична карта\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Употреба на GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Мрежово\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Здраве\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Мониторинг на heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat е изпратен успешно\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Команда Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Хост / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP метод\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP метод: POST, GET или HEAD (по подразбиране: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Неактивна\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Ако си загубил паролата до администраторския акаунт, можеш да я нулираш със следващата команда.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Образ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Неактивен\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Интервал\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Невалиден имейл адрес.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Език\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Подреждане\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Ширина на макета\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Жизнен цикъл\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"лимит\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Средно натоварване\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Средно натоварване 15 минути\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Средно натоварване 1 минута\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Средно натоварване 5 минути\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Средно натоварване\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Състояние на зареждане\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Зареждане...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Изход\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Вход\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Неуспешен опит за вход\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Логове\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Търсиш къде да създадеш тревоги? Натисни емотиконата за звънец <0/> в таблицата за системи.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Главен PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Управление на предпочитанията за показване и уведомяване.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Инструкции за ръчна настройка\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Максимум 1 минута\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Памет\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Лимит на памет\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Пик на памет\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Употреба на паметта\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Използването на памет от docker контейнерите\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Модел\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Име\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Мрежа\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Мрежов трафик на docker контейнери\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Мрежов трафик на публични интерфейси\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Единица за измерване на скорост\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Не\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Няма намерени резултати.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Няма резултати.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Няма налични S.M.A.R.T. атрибути за това устройство.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Няма намерени системи.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Нотификации\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Поддръжка на OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"На всеки рестарт, системите в датабазата ще бъдат обновени да съвпадат със системите зададени във файла.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Еднократен\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Еднократна парола\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Отвори менюто\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Или продължи с\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Други\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Презапиши съществуващи тревоги\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Страница\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Страница {0} от {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Страници / Настройки\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Парола\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Паролата трябва да е поне 8 символа.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Паролата трябва да е по-малка от 72 байта.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Получено е искането за нулиране на паролата\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Минал\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Пауза\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"На пауза\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"На пауза ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Формат на полезния товар\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Средно използване на ядро\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Процент време, прекарано във всяко състояние\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Постоянен\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Устойчивост\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Моля <0>конфигурурай SMTP сървър</0> за да се подсигуриш, че тревогите са доставени.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Моля провери log-овете за повече информация.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Моля провери дадената информация и опитай отново\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Моля създай администраторски акаунт\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Моля активирай изскачащите прозорци за този сайт\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Моля влез отново\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Моля виж <0>документацията</0> за инструкции.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Моля влез в акаунта ти\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Порт\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Включване\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Точно използване в записаното време\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Предпочитан език\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Процесът стартира\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Публичен ключ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Тихи часове\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Прочети\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Получени\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Опресни\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Връзки\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Заявка за еднократна парола\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Заявка OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Изисква се от\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Изисква\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Нулиране на парола\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Решен\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Рестартирания\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Възобнови\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Корен\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Пресъздаване на идентификатора\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Редове на страница\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Метрики на изпълнение\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Детайли\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Самотест\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Запази адреса с enter или запетая. Остави празно за да изключиш нотификациите чрез имейл.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Запази настройките\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Запази система\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Запазен е в базата данни и не изтича, докато не го деактивирате.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"График\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Планирай тихи часове, когато няма да се изпращат известия, като например по време на периоди на поддръжка.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Планирай тихи часове, когато няма да се изпращат известия.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Търси\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Търси за системи или настройки...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Секунди между пинговете (по подразбиране: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Виж <0>настройките за нотификациите</0> за да конфигурираш как получаваш тревоги.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Избери {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Изпратете единичен heartbeat пинг, за да проверите дали вашата крайна точка работи.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Изпращайте периодични изходящи пингове към външна услуга за мониторинг, за да можете да наблюдавате Beszel, без да го излагате на интернет.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Изпращане на тестов heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Изпратени\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Сериен номер\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Детайли на услугата\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Услуги\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Задайте процентни прагове за цветовете на измервателните уреди.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Задайте следните променливи на средата на вашия Beszel hub, за да активирате мониторинга на heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Настройки\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Настройките са запазени\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Влез\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Настройки за SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Сортиране по\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Начален час\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Състояние\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Статус\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Подсъстояние\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Изполван swap от системата\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Използване на swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Система\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Средно натоварване на системата във времето\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Услуги на systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Системи\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Системите могат да бъдат управлявани в <0>config.yml</0> файл намиращ се в директорията с данни.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Таблица\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Задачи\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Температура\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Температура\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Единица за температура\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Температири на системни сензори\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Тествай <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Тестов heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Тестова нотификация изпратена\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Общият статус е <0>ok</0>, когато всички системи работят, <1>warn</1>, когато са задействани предупреждения, и <2>error</2>, когато някоя система е спряла.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"След това влез в backend-а и нулирай паролата за потребителския акаунт в таблицата за потребители.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Това действие не може да бъде отменено. Това ще изтрие всички записи за {name} от датабазата.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Това ще доведе до трайно изтриване на всички избрани записи от базата данни.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Пропускателна способност на {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Пропускателна способност на root файловата система\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Формат на времето\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"До имейл(ите)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Превключване на мрежа\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Включи тема\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Токен\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Токен & Пръстов отпечатък\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Токените позволяват на агентите да се свързват и регистрират. Отпечатъците са стабилни идентификатори, уникални за всяка система, които се задават при първото свързване.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Токените и пръстовите отпечатъци се използват за удостоверяване на WebSocket връзките към концентратора.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Общо\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Общо получени данни за всеки интерфейс\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Общо изпратени данни за всеки интерфейс\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Общо: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Активиран от\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Активатори\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на паметта за 1 минута надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на паметта за 15 минута надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на паметта за 5 минута надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Задейства се, когато някой даден сензор надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Задейства се, когато зарядът на батерията падне под зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Задейства се, когато комбинираното качване/сваляне надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на процесора надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Задейства се, когато използването на GPU надвиши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на паметта надвиши зададен праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Задейства се, когато статуса превключва между долу и горе\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Задейства се, когато употребата на някой диск надивши зададен праг\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Тип\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Файл на единица\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Предпочитания на единицата\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Универсален тоукън\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Неизвестна\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Неограничено\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Нагоре\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Нагоре ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Актуализирай\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Актуализирано\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Актуализира се на всеки 10 минути.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Качване\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Употреба\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Употреба на root partition-а\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Използвани\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Потребители\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Стойност\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Изглед\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Виж повече\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Прегледайте последните си 200 сигнала.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Видими полета\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Изчаква се за достатъчно записи за показване\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Искаш да помогнеш да направиш преводите още по-добри? Провери нашия <0>Crowdin</0> за повече детайли.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Иска\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Предупреждение (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Прагове за предупреждение\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Пуш нотификации\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Когато е активиран, този символ позволява на агентите да се регистрират сами без предварително създаване на система.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"При използване на POST всеки heartbeat включва JSON полезен товар с резюме на състоянието на системата, списък на спрените системи и задействаните предупреждения.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Команда Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Запиши\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML конфигурация\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML конфигурация\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Да\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Настройките за потребителя ти са обновени.\"\n"
  },
  {
    "path": "internal/site/src/locales/cs/cs.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: cs\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Czech\\n\"\n\"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: cs\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} z {1} vybraných řádků.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# jádro} few {# jádra} many {# jader} other {# jader}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} den} few {{countString} dny} other {{countString} dní}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} Hodina} few {{countString} Hodiny} many {{countString} Hodin} other {{countString} Hodin}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuta} few {{countString} minuty} many {{countString} minut} other {{countString} minut}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# vlákno} few {# vlákna} many {# vláken} other {# vláken}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 hodina\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuta\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 týden\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 hodin\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 hodin\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dní\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Akce\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktivní\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktivní výstrahy\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktivní stav\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Přidat {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Přidat <0>Systém</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Přidat systém\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Přidat URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Upravit možnosti zobrazení pro grafy.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Upravit šířku hlavního rozvržení\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Administrátor\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Po\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Po nastavení proměnných prostředí restartujte hub Beszel, aby se změny projevily.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Historie upozornění\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Výstrahy\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Všechny kontejnery\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Všechny systémy\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Opravdu chcete odstranit {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Jste si jistý?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatická kopie vyžaduje zabezpečený kontext.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Průměr\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Průměrné využití CPU kontejnerů\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Průměr klesne pod <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Průměr je vyšší než <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Průměrná spotřeba energie GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Průměrné využití CPU v celém systému\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Průměrné využití {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Průměrné využití GPU engine\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Zálohy\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Přenos\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Baterie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Stal se aktivním\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Stal se neaktivním\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Před\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Pod {0}{1} za {2, plural, one {poslední # minutu} few {poslední # minuty} other {posledních # minut}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel podporuje OpenID Connect a mnoho poskytovatelů OAuth2 ověřování.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel používá <0>Shoutrrr</0> k integraci s populárními notifikačními službami.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binární\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bity (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Stav zavádění\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Byty (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / vyrovnávací paměť\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Může znovu načíst\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Může spustit\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Může zastavit\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Zrušit\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Schopnosti\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapacita\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Upozornění - možná ztráta dat\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsia (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Změnit jednotky zobrazení metrik.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Změnit obecné nastavení aplikace.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Nabíjení\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Nabíjení\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Možnosti grafu\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Zkontrolujte {email} pro odkaz na obnovení.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Pro více informací zkontrolujte logy.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Zkontrolujte svou monitorovací službu\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Zkontrolujte službu upozornění\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Vymazat\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klikněte na kontejner pro zobrazení dalších informací.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klikněte na zařízení pro zobrazení dalších informací.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klikněte na systém pro zobrazení více informací.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klikněte pro zkopírování\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instrukce příkazového řádku\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurace způsobu přijímání upozornění.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Potvrdit heslo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikty\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Připojení je nedostupné\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Pokračovat\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Zkopírováno do schránky\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopírovat docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Zkopírovat příkaz na spuštění dockeru\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopírovat env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopírovat hostitele\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopírovat příkaz Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopírovat název\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopírovat text\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Zkopírujte instalační příkaz pro agenta níže nebo automaticky registrujte agenty s <0>univerzálním token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Zkopírujte obsah <0>docker-compose.yml</0> pro agenta níže nebo automaticky registrujte agenty s <1>univerzálním token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopírovat YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"Procesor\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU jádra\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Špička CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Čas CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Rozdělení času CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Využití procesoru\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Vytvořit\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Vytvořit účet\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Vytvořeno\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritické (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativní stažení\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativní odeslání\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Aktuální stav\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cykly\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Denně\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Výchozí doba\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Odstranit\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Smazat identifikátor\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Popis\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detail\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Zařízení\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Vybíjení\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Disková jednotka\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Využití disku\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Využití disku {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Využití CPU Dockeru\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Využití paměti Dockeru\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Síťové I/O Dockeru\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentace\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nefunkční\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nefunkční ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Stažení\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Doba trvání\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Upravit\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Upravit {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-mail\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Emailová upozornění\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Prázdná\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Čas ukončení\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL koncového bodu\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL koncového bodu pro ping (vyžadováno)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Zadejte e-mailovou adresu pro obnovu hesla\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Zadejte e-mailovou adresu...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Zadejte Vaše jednorázové heslo.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Efemérní\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Chyba\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Příklad:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Překračuje {0}{1} za {2, plural, one {poslední # minutu} few {poslední # minuty} other {posledních # minut}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Hlavní PID spuštění\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Stávající systémy, které nejsou definovány v <0>config.yml</0>, budou odstraněny. Provádějte pravidelné zálohování.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Ukončeno aktivně\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Vyprší po jedné hodině nebo při restartu hubu.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportovat\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Exportovat konfiguraci\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exportovat aktuální konfiguraci systémů.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheita (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Selhalo\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Neúspěšné atributy:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Ověření se nezdařilo\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Nepodařilo se uložit nastavení\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Nepodařilo se odeslat heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Nepodařilo se odeslat testovací oznámení\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Nepodařilo se aktualizovat upozornění\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Neúspěšné: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtr...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Otisk\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Za <0>{min}</0> {min, plural, one {minutu} few {minuty} other {minut}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Zapomněli jste heslo?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD příkaz\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Plná\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Obecné\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globální\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU enginy\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Spotřeba energie GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Využití GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Mřížka\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Zdraví\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitorování heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat úspěšně odeslán\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew příkaz\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Hostitel / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP metoda\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP metoda: POST, GET nebo HEAD (výchozí: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Neaktivní\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Pokud jste ztratili heslo k vašemu účtu správce, můžete jej obnovit pomocí následujícího příkazu.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Obraz\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Neaktivní\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Neplatná e-mailová adresa.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Jazyk\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Rozvržení\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Šířka rozvržení\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Životní cyklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Průměrné vytížení\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Průměrná zátěž 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Průměrná zátěž 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Průměrná zátěž 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Prům. zatížení\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Stav načtení\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Načítání...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Odhlásit\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Přihlásit\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Pokus o přihlášení selhal\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logy\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Hledáte místo kde vytvářet upozornění? Klikněte na ikonu zvonku <0/> v systémové tabulce.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Hlavní PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Správa nastavení zobrazení a oznámení.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Pokyny k manuálnímu nastavení\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max. 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Paměť\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Limit paměti\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Špička paměti\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Využití paměti\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Využití paměti docker kontejnerů\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Název\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Síť\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Síťový provoz kontejnerů docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Síťový provoz veřejných rozhraní\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Síťová jednotka\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Ne\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nenalezeny žádné výskyty.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Žádné výsledky.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Pro toto zařízení nejsou k dispozici žádné atributy S.M.A.R.T.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nenalezeny žádné systémy.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Upozornění\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Podpora OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Při každém restartu budou systémy v databázi aktualizovány tak, aby odpovídaly systémům definovaným v souboru.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Jednorázové\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Jednorázové heslo\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Otevřít menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Nebo pokračujte s\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Jiné\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Přepsat existující upozornění\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Stránka\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Stránka {0} z {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Stránky / Nastavení\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Heslo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Heslo musí obsahovat alespoň 8 znaků.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Heslo musí být menší než 72 bytů.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Žádost o obnovu hesla byla přijata\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Minulé\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pozastavit\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pozastaveno\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pozastaveno ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Formát payloadu\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Průměrné využití na jádro\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Procento času strávěného v každém stavu\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Trvalý\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Trvalost\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"<0>nakonfigurujte SMTP server</0> pro zajištění toho, aby byla upozornění doručena.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Pro více informací zkontrolujte logy.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Zkontrolujte prosím Vaše přihlašovací údaje a zkuste to znovu\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Vytvořte si prosím účet administrátora\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Prosím povolte vyskakovací okna pro tento web\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Přihlaste se prosím znovu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Instrukce naleznete v <0>dokumentaci</0>.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Přihlaste se prosím k vašemu účtu\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Zapnutí\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Přesné využití v zaznamenaném čase\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Upřednostňovaný jazyk\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces spuštěn\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Veřejný klíč\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Tiché hodiny\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Číst\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Přijato\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Aktualizovat\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Vztahy\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Požádat o jednorázové heslo\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Požádat OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Vyžadováno službou\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Vyžaduje\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Obnovit heslo\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Vyřešeno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Restarty\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Pokračovat\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Kořenový\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Změnit token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Řádků na stránku\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metriky běhu\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Detaily\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Vlastní test\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Adresu uložte pomocí klávesy enter nebo čárky. Pro deaktivaci e-mailových oznámení ponechte prázdné pole.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Uložit nastavení\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Uložit systém\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Uložen v databázi a nevyprší, dokud jej nezablokujete.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Plán\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Naplánujte tiché hodiny, kdy se nebudou odesílat oznámení, například během období údržby.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Naplánujte tiché hodiny, kdy se nebudou odesílat oznámení.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Hledat\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Hledat systémy nebo nastavení...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekundy mezi pingy (výchozí: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Podívejte se na <0>nastavení upozornění</0> pro nastavení toho, jak přijímáte upozornění.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Vybrat {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Odešlete jeden heartbeat ping pro ověření funkčnosti vašeho koncového bodu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Odesílejte periodické odchozí pingy na externí monitorovací službu, abyste mohli monitorovat Beszel bez jeho vystavení internetu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Odeslat testovací heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Odeslat\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Sériové číslo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Detaily služby\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Služby\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Nastavte procentuální prahové hodnoty pro barvy měřičů.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Pro povolení monitorování heartbeat nastavte na hubu Beszel následující proměnné prostředí:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Nastavení\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Nastavení uloženo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Přihlásit se\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Nastavení SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Seřadit podle\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Čas začátku\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Stav\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Stav\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Podstav\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap prostor využívaný systémem\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap využití\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Systém\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Průměry zatížení systému v průběhu času\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Služby systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systémy\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systémy lze spravovat v souboru <0>config.yml</0> uvnitř datového adresáře.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabulka\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Úlohy\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Teplota\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Teplota\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Jednotky teploty\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Teploty systémových senzorů\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Testovat <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testovat heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testovací oznámení odesláno\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Celkový stav je <0>ok</0>, když jsou všechny systémy v provozu, <1>warn</1>, když jsou spuštěny výstrahy, a <2>error</2>, když je některý systém mimo provoz.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Poté se přihlaste do backendu a obnovte heslo k uživatelskému účtu v tabulce uživatelů.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Tuto akci nelze vzít zpět. Tím se z databáze trvale odstraní všechny aktuální záznamy pro {name}.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Tímto trvale odstraníte všechny vybrané záznamy z databáze.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Propustnost {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Propustnost kořenového souborového systému\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Formát času\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Na email(y)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Přepnout mřížku\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Přepnout motiv\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokeny & Otisky\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokeny umožňují agentům připojení a registraci. Otisky jsou stabilní identifikátory jedinečné pro každý systém, nastavené na první připojení.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokeny a otisky slouží k ověření připojení WebSocket k uzlu.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Celkem\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Celkový přijatý objem dat pro každé rozhraní\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Celkový odeslaný objem dat pro každé rozhraní\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Celkem: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Spuštěno službou\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Spouštěče\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Spustí se, když využití paměti během 1 minuty překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Spustí se, když využití paměti během 15 minut překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Spustí se, když využití paměti během 5 minut překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Spustí se, když některý senzor překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Spustí se, když úroveň nabití baterie klesne pod prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Spustí se, když kombinace up/down překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Spustí se, když využití procesoru překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Spustí se, když využití GPU překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Spustí se, když využití paměti překročí prahovou hodnotu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Spouští se, když se změní dostupnost\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Spustí se, když využití disku překročí prahovou hodnotu\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Typ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Soubor jednotky\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Předvolby jednotek\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Univerzální token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Neznámá\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Neomezeno\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Funkční\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Funkční ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Aktualizovat\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Aktualizováno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Aktualizováno každých 10 minut.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Odeslání\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Využití\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Využití kořenového oddílu\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Využito\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Uživatelé\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Hodnota\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Zobrazení\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Zobrazit více\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Zobrazit vašich 200 nejnovějších upozornění.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Viditelné sloupce\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Čeká se na dostatek záznamů k zobrazení\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Chcete nám pomoci s našimi překlady ještě lépe? Podívejte se na <0>Crowdin</0> pro více informací.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Chce\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Varování (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Prahové hodnoty pro varování\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push oznámení\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Pokud je povoleno, umožňuje tento token agentům samo-registraci bez předchozího vytvoření systému.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Při použití metody POST obsahuje každý heartbeat JSON payload se souhrnem stavu systému, seznamem nefunkčních systémů a spuštěnými výstrahami.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows příkaz\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Psát\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML konfigurace\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML konfigurace\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ano\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Vaše uživatelská nastavení byla aktualizována.\"\n"
  },
  {
    "path": "internal/site/src/locales/da/da.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: da\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Danish\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: da\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} af {1} række(r) valgt.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# kerne} other {# kerner}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dag} other {{countString} dage}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} time} other {{countString} timer}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minut} other {{countString} minutter}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# tråd} other {# tråde}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 time\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 minut\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minut\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 uge\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 timer\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 minutter\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 timer\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dage\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 minutter\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Handlinger\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktiv\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktive Alarmer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktiv tilstand\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Tilføj {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Tilføj <0>System</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Tilføj system\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Tilføj URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Juster visningsindstillinger for diagrammer.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Juster bredden af hovedlayoutet\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Administrator\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Efter\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Efter indstilling af miljøvariablerne skal du genstarte din Beszel-hub for at ændringerne kan træde i kraft.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Advarselshistorik\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alarmer\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Alle containere\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Alle systemer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Er du sikker på, at du vil slette {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Er du sikker?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatisk kopiering kræver en sikker kontekst.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Gennemsnitlig\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Gennemsnitlig CPU udnyttelse af containere\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Gennemsnit falder under <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Gennemsnittet overstiger <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Gennemsnitligt strømforbrug for GPU'er\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Gennemsnitlig systembaseret CPU-udnyttelse\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Gennemsnitlig udnyttelse af {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Gennemsnitlig udnyttelse af GPU-enheder\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Sikkerhedskopier\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Båndbredde\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batteri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Blev aktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Blev inaktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Før\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Under {0}{1} i sidste {2, plural, one {# minut} other {# minutter}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel understøtter OpenID Connect og mange OAuth2 godkendelsesudbydere.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel bruger <0>Shoutrrr</0> til at integrere med populære notifikationstjenester.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binær\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Opstartstilstand\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffere\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Kan genindlæse\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Kan starte\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Kan stoppe\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Fortryd\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Funktioner\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapacitet\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Forsigtig - muligt tab af data\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Ændre viste enheder for målinger.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Skift generelle applikationsindstillinger.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Opladning\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Oplader\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Diagrammuligheder\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Tjek {email} for et nulstillingslink.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Tjek logfiler for flere detaljer.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Tjek din overvågningstjeneste\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Tjek din notifikationstjeneste\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Ryd\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klik på en container for at se mere information.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klik på en enhed for at se flere oplysninger.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klik på et system for at se mere information.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klik for at kopiere\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instruktioner for kommandolinje\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurer hvordan du modtager advarselsmeddelelser.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Bekræft adgangskode\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikter\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Forbindelsen er nede\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Fortsæt\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Kopieret til udklipsholder\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopiér docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Kopiér docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopier miljø\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopier vært\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopier Linux kommando\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopier navn\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopier tekst\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopier installationskommandoen for agenten nedenfor, eller registrer agenter automatisk med en <0>universalnøgle</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopier <0>docker-compose.yml</0> indholdet for agenten nedenfor, eller registrer agenter automatisk med en <1>universalnøgle</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopier YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU-kerner\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU Peak\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU tid\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU-tidsfordeling\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU forbrug\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Opret\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Opret konto\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Oprettet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritisk (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativ download\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativ upload\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Nuværende tilstand\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cykler\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Dagligt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standard tidsperiode\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Slet\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Slet fingeraftryk\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Beskrivelse\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detalje\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Enhed\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Aflader\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Diskenhed\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Diskforbrug\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Diskforbrug af {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU forbrug\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker Hukommelsesforbrug\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Netværk I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentation\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nede\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nede ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Hent ned\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Varighed\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Rediger\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Rediger {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Email-notifikationer\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Tom\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Sluttid\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Endpoint-URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Endpoint-URL til ping (påkrævet)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Indtast emailadresse for at nulstille adgangskoden\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Indtast emailadresse...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Indtast din engangsadgangskode.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Efemer\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Fejl\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Eksempel:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Overskrider {0}{1} i sidste {2, plural, one {# minut} other {# minutter}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Exec vigtigste PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Eksisterende systemer ikke defineret i <0>config.yml</0> vil blive slettet. Opret venligst regelmæssige sikkerhedskopier.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Afsluttet aktiv\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Udløber efter en time eller ved hub-genstart.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Eksporter\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Eksporter konfiguration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Eksporter din nuværende systemkonfiguration.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Mislykkedes\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Mislykkede attributter:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Kunne ikke godkende\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Kunne ikke gemme indstillinger\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Kunne ikke sende heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Afsendelse af testnotifikation mislykkedes\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Kunne ikke opdatere alarm\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Mislykkedes: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filter...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingeraftryk\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"For <0>{min}</0> {min, plural, one {minut} other {minutter}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Glemt adgangskode?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD kommando\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Fuldt opladt\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Generelt\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-enheder\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU Strøm Træk\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU-forbrug\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Gitter\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Sundhed\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat-overvågning\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat sendt succesfuldt\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew-kommando\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Vært / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-metode\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-metode: POST, GET eller HEAD (standard: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inaktiv\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Hvis du har mistet adgangskoden til din administratorkonto, kan du nulstille den ved hjælp af følgende kommando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Billede\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inaktiv\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Ugyldig email adresse.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Sprog\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Opstilling\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layoutbredde\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Livscyklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"grænse\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Belastning Gennemsnitlig\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Belastning Gennemsnitlig 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Belastning Gennemsnitlig 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Belastning Gennemsnitlig 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Belastning gns.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Indlæsningstilstand\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Indlæser...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Log ud\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Log ind\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Loginforsøg mislykkedes\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logs\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Leder du i stedet for efter hvor du kan oprette alarmer? Klik på klokken <0/> ikoner i system tabellen.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Primær PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Administrer display og notifikationsindstillinger.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Manuel opsætningsvejledning\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maks. 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Hukommelse\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Hukommelsesgrænse\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Hukommelsesspids\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Hukommelsesforbrug\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Hukommelsesforbrug af dockercontainere\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Navn\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Net\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Netværkstrafik af dockercontainere\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Netværkstrafik af offentlige grænseflader\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Netværksenhed\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nej\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Ingen resultater fundet.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Ingen resultater.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Ingen S.M.A.R.T.-attributter tilgængelige for denne enhed.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Ingen systemer fundet.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notifikationer\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC understøttelse\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Ved hver genstart vil systemer i databasen blive opdateret til at matche de systemer, der er defineret i filen.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Engangs\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Engangsadgangskode\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Åbn menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Eller fortsæt med\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Andre\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Overskriv eksisterende alarmer\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Side\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Side {0} af {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Sider / Indstillinger\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Adgangskode\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Adgangskoden skal være på mindst 8 tegn.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Adgangskoden skal være mindre end 72 bytes.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Anmodning om nulstilling af adgangskode modtaget\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Tidligere\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pause\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Sat på pause\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Sat på pause ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Payload-format\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Gennemsnitlig udnyttelse pr. kerne\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Procentdel af tid brugt i hver tilstand\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Vedholdenhed\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Konfigurer <0>en SMTP server</0> for at sikre at alarmer bliver leveret.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Tjek logfiler for flere detaljer.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Tjek dine legitimationsoplysninger og prøv igen\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Opret venligst en administratorkonto\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Aktiver pop-ups for dette websted\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Log venligst ind igen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Se <0>dokumentationen</0> for instruktioner.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Log venligst ind på din konto\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Tænd\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Præcis udnyttelse på det registrerede tidspunkt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Foretrukket sprog\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces startet\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Offentlig nøgle\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Stille timer\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Læs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Modtaget\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Opdater\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relationer\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Anmod om engangsadgangskode\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Anmod OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Kræves af\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Kræver\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Nulstil adgangskode\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Løst\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Genstarter\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Genoptag\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Roter nøgle\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Rækker per side\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Køretidsmålinger\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.-detaljer\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. selvtest\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Gem adresse ved hjælp af enter eller komma. Lad feltet stå tomt for at deaktivere e-mail-meddelelser.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Gem indstillinger\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Gem system\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Gemt i databasen og udløber ikke, før du deaktiverer det.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Planlæg\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Planlæg stille timer hvor meddelelser ikke vil blive sendt, såsom under vedligeholdelsesperioder.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Planlæg stille timer hvor meddelelser ikke vil blive sendt.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Søg\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Søg efter systemer eller indstillinger...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunder mellem pings (standard: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Se <0>meddelelsesindstillinger</0> for at konfigurere, hvordan du modtager alarmer.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Vælg {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Send et enkelt heartbeat-ping for at bekræfte, at dit endpoint fungerer.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Send periodiske udgående pings til en ekstern overvågningstjeneste, så du kan overvåge Beszel uden at eksponere det for internettet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Send test-heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Sendt\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serienummer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Tjenestedetaljer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Tjenester\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Indstil procentvise tærskler for målerfarver.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Indstil følgende miljøvariabler på din Beszel-hub for at aktivere heartbeat-overvågning:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Indstillinger\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Indstillinger gemt\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Log ind\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP-indstillinger\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sorter efter\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Starttid\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Tilstand\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Undertilstand\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap plads brugt af systemet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap forbrug\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Gennemsnitlig system belastning over tid\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd Services\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systemer\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systemer kan være administreres i filen <0>config.yml</0> i din datamappe.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabel\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Opgaver\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperaturenhed\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturer i systemsensorer\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test-heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Test notifikation sendt\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Den overordnede status er <0>ok</0>, når alle systemer kører, <1>warn</1>, når alarmer udløses, og <2>error</2>, når et system er nede.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Log derefter ind på backend og nulstil adgangskoden til din brugerkonto i tabellen brugere.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Denne handling kan ikke fortrydes. Dette vil permanent slette alle aktuelle elementer for {name} fra databasen.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Dette vil permanent slette alle poster fra databasen.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Gennemløb af {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Gennemløb af rodfilsystemet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Tidsformat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Til email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Slå gitter til/fra\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Skift tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Nøgle\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Nøgler & fingeraftryk\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Nøgler tillader agenter at oprette forbindelse og registrere. Fingeraftryk er stabile identifikatorer unikke for hvert system, indstillet ved første forbindelse.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Nøgler og fingeraftryk bruges til at godkende WebSocket-forbindelser til hubben.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Samlet\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Samlet modtaget data for hver interface\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Samlet sendt data for hver interface\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"I alt: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Udløst af\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Udløsere\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Udløser når 1 minut belastning gennemsnit overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Udløser når 15 minut belastning gennemsnit overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Udløser når 5 minut belastning gennemsnit overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Udløser når en sensor overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Udløses når batteriniveauet falder under en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Udløses når de kombinerede op/ned overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Udløser når CPU-forbrug overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Udløses når GPU-brug overstiger en grænse\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Udløser når hukommelsesforbruget overstiger en tærskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Udløser når status skifter mellem op og ned\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Udløser når brugen af en disk overstiger en tærskel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Type\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Enhed fil\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Enhedspræferencer\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universalnøgle\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Ukendt\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Ubegrænset\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Oppe\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Oppe ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Opdater\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Opdateret\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Opdateret hver 10. minut.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Overfør\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Oppetid\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Forbrug\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Brug af rodpartition\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Brugt\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Brugere\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Værdi\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Vis\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Se mere\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Se dine 200 nyeste alarmer.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Synlige felter\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Venter på nok posteringer til at vise\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Vil du hjælpe os med at gøre vores oversættelser endnu bedre? Tjek <0>Crowdin</0> for flere detaljer.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Ønsker\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Advarsel (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Advarselstærskler\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push notifikationer\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Når aktiveret, tillader denne token agenter at registrere sig selv uden forudgående systemoprettelse.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Når du bruger POST, inkluderer hvert heartbeat en JSON-payload med resumé af systemstatus, liste over systemer, der er nede, og udløste alarmer.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows-kommando\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Skriv\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML Konfiguration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Konfiguration\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Dine brugerindstillinger er opdateret.\"\n"
  },
  {
    "path": "internal/site/src/locales/de/de.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: de\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: German\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: de\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} von {1} Zeile(n) ausgewählt.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# Kern} other {# Kerne}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} Tag} other {{countString} Tage}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} Stunde} other {{countString} Stunden}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} Minute} other {{countString} Minuten}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# Thread} other {# Threads}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 Stunde\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 Min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 Minute\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 Woche\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 Stunden\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 Min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 Stunden\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 Tage\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 Min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Aktionen\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktiv\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktive Warnungen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktiver Zustand\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"{foo} hinzufügen\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"<0>System</0> hinzufügen\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"System hinzufügen\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"URL hinzufügen\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Anzeigeoptionen für Diagramme anpassen.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Breite des Hauptlayouts anpassen\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Nach\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Starten Sie nach dem Festlegen der Umgebungsvariablen Ihren Beszel-Hub neu, damit die Änderungen wirksam werden.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Warnungsverlauf\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Warnungen\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Alle Container\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Alle Systeme\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Möchtest du {name} wirklich löschen?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Bist du sicher?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatisches Kopieren erfordert einen sicheren Kontext.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Durchschnitt\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Durchschnittliche CPU-Auslastung der Container\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Durchschnitt unterschreitet <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Durchschnitt überschreitet <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Durchschnittlicher Stromverbrauch der GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Durchschnittliche systemweite CPU-Auslastung\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Durchschnittliche Auslastung von {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Durchschnittliche Auslastung der GPU-Engines\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Backups\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandbreite\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batterie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Wurde aktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Wurde inaktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Vor\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Unterschreitet {0}{1} in den letzten {2, plural, one {# Minute} other {# Minuten}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel unterstützt OpenID Connect und viele OAuth2-Authentifizierungsanbieter.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel verwendet <0>Shoutrrr</0>, um sich mit beliebten Benachrichtigungsdiensten zu integrieren.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binär\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Boot-Zustand\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Puffer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Kann neu laden\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Kann starten\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Kann stoppen\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Abbrechen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Fähigkeiten\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapazität\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Vorsicht - potenzieller Datenverlust\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Anzeigeeinheiten der Werte ändern.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Allgemeine Anwendungsoptionen ändern.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Ladung\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Wird geladen\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Diagrammoptionen\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Überprüfe {email} auf einen Link zum Zurücksetzen.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Überprüfe die Protokolle für weitere Details.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Überprüfen Sie Ihren Überwachungsdienst\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Überprüfe deinen Benachrichtigungsdienst\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Löschen\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klicke auf einen Container, um weitere Informationen zu sehen.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klicke auf ein Gerät, um weitere Informationen zu sehen.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klicke auf ein System, um weitere Informationen zu sehen.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Zum Kopieren klicken\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Befehlszeilenanweisungen\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfiguriere, wie du Warnbenachrichtigungen erhältst.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Passwort bestätigen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikte\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Verbindung unterbrochen\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Fortfahren\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"In die Zwischenablage kopiert\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Docker compose kopieren\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Docker run kopieren\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Umgebungsvariablen kopieren\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Host kopieren\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Linux-Befehl kopieren\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Name kopieren\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Text kopieren\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopiere den Installationsbefehl für den Agent unten oder registriere Agents automatisch mit einem <0>universellen Token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopiere den<0>docker-compose.yml</0> Inhalt für den Agent unten oder registriere Agents automatisch mit einem <1>universellen Token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAML kopieren\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU-Kerne\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU-Spitze\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU-Zeit\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU-Zeit-Aufschlüsselung\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU-Auslastung\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Erstellen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Konto erstellen\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Erstellt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritisch (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativer Download\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativer Upload\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Aktueller Zustand\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Zyklen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Täglich\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standardzeitraum\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Löschen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Fingerabdruck löschen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Beschreibung\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Details\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Gerät\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Wird entladen\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Festplatte\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Festplatten-I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Festplatteneinheit\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Festplattennutzung\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Festplattennutzung von {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker-CPU-Auslastung\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker-Arbeitsspeichernutzung\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker-Netzwerk-I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentation\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Inaktiv\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Inaktiv ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Herunterladen\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Dauer\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Bearbeiten\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"{foo} bearbeiten\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-Mail\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-Mail-Benachrichtigungen\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Leer\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Endzeit\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Endpunkt-URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Endpunkt-URL zum Pingen (erforderlich)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"E-Mail-Adresse eingeben, um das Passwort zurückzusetzen\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"E-Mail-Adresse eingeben...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Geben Sie Ihr Einmalpasswort ein.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Flüchtig\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Fehler\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Beispiel:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Überschreitet {0}{1} in den letzten {2, plural, one {# Minute} other {# Minuten}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Ausführungs-Haupt-PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Bestehende Systeme, die nicht in der <0>config.yml</0> definiert sind, werden gelöscht. Bitte mache regelmäßige Backups.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Beendet aktiv\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Läuft nach einer Stunde oder bei Hub-Neustart ab.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportieren\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Konfiguration exportieren\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exportiere die aktuelle Systemkonfiguration.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Fehlgeschlagen\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Fehlgeschlagene Attribute:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Authentifizierung fehlgeschlagen\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Einstellungen konnten nicht gespeichert werden\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Heartbeat konnte nicht gesendet werden\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Testbenachrichtigung konnte nicht gesendet werden\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Warnung konnte nicht aktualisiert werden\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Fehlgeschlagen: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filter...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingerabdruck\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firm­ware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Für <0>{min}</0> {min, plural, one {Minute} other {Minuten}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Passwort vergessen?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD Befehl\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Voll\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Allgemein\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-Engines\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU-Leistungsaufnahme\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU-Auslastung\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Raster\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Gesundheit\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat-Überwachung\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat erfolgreich gesendet\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew-Befehl\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-Methode\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-Methode: POST, GET oder HEAD (Standard: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Untätig\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Wenn du das Passwort für dein Administratorkonto verloren hast, kannst du es mit dem folgenden Befehl zurücksetzen.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Image\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inaktiv\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervall\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Ungültige E-Mail-Adresse.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Sprache\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Anordnung\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layoutbreite\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Lebenszyklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"Limit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Durchschnittliche Systemlast\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Durchschnittliche Systemlast 15 Min\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Durchschnittliche Systemlast 1 Min\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Durchschnittliche Systemlast 5 Min\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Systemlast\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Ladezustand\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Lädt...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Abmelden\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Anmelden\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Anmeldeversuch fehlgeschlagen\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Protokolle\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Du möchtest neue Warnungen erstellen? Klicke dafür auf die Glocken-<0/>-Symbole in der Systemtabelle.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Haupt-PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Anzeige- und Benachrichtigungseinstellungen verwalten.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Anleitung zur manuellen Einrichtung\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 Min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Arbeitsspeicher\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Arbeitsspeicherlimit\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Arbeitsspeicher-Spitze\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Arbeitsspeichernutzung\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Arbeitsspeichernutzung der Docker-Container\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modell\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Name\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Netzwerk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Netzwerkverkehr der Docker-Container\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Netzwerkverkehr der öffentlichen Schnittstellen\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Netzwerkeinheit\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nein\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Keine Ergebnisse gefunden.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Keine Ergebnisse.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Für dieses Gerät sind keine S.M.A.R.T.-Attribute verfügbar.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Keine Systeme gefunden.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Benachrichtigungen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC-Unterstützung\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Bei jedem Neustart werden die Systeme in der Datenbank aktualisiert, um den in der Datei definierten Systemen zu entsprechen.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Einmalig\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Einmalpasswort\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Menü öffnen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Oder fortfahren mit\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Andere\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Bestehende Warnungen überschreiben\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Seite\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Seite {0} von {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Seiten / Einstellungen\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Passwort\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Das Passwort muss mindestens 8 Zeichen haben.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Das Passwort muss weniger als 72 Bytes lang sein.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Anfrage zum Zurücksetzen des Passworts erhalten\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Vergangen\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pause\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pausiert\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pausiert ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Payload-Format\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Durchschnittliche Auslastung pro Kern\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Prozentsatz der Zeit in jedem Zustand\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistenz\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Bitte <0>konfiguriere einen SMTP-Server</0>, um sicherzustellen, dass Warnungen zugestellt werden.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Bitte überprüfe die Protokolle für weitere Details.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Bitte überprüfe deine Anmeldedaten und versuche es erneut\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Bitte erstelle ein Administratorkonto\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Bitte aktiviere Pop-ups für diese Seite\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Bitte melde dich erneut an\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"In der <0>Dokumentation</0> findest du weitere Anweisungen.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Bitte melde dich bei deinem Konto an\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Eingeschaltet\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Genaue Nutzung zum aufgezeichneten Zeitpunkt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Bevorzugte Sprache\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Prozess gestartet\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Öffentlicher Schlüssel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Ruhezeiten\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lesen\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Empfangen\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Aktualisieren\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Beziehungen\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Einmalpasswort anfordern\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP anfordern\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Benötigt von\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Benötigt\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Passwort zurücksetzen\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Gelöst\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Neustarts\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Fortsetzen\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Token rotieren\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Zeilen pro Seite\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Laufzeitmetriken\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.-Details\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T.-Selbsttest\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Adresse mit der Enter-Taste oder Komma speichern. Leer lassen, um E-Mail-Benachrichtigungen zu deaktivieren.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Einstellungen speichern\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"System speichern\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"In der Datenbank gespeichert und läuft nicht ab, bis Sie es deaktivieren.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Zeitplan\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Plane Ruhezeiten, in denen keine Benachrichtigungen gesendet werden, z. B. während Wartungszeiten.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Plane Ruhezeiten, in denen keine Benachrichtigungen gesendet werden.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Suche\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Nach Systemen oder Einstellungen suchen...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunden zwischen Pings (Standard: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Siehe <0>Benachrichtigungseinstellungen</0>, um zu konfigurieren, wie du Warnungen erhältst.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Auswählen {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Senden Sie einen einzelnen Heartbeat-Ping, um zu überprüfen, ob Ihr Endpunkt funktioniert.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Senden Sie regelmäßige ausgehende Pings an einen externen Überwachungsdienst, damit Sie Beszel überwachen können, ohne es dem Internet auszusetzen.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Test-Heartbeat senden\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Gesendet\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Seriennummer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Servicedetails\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Dienste\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Prozentuale Schwellenwerte für Zählerfarben festlegen.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Legen Sie die folgenden Umgebungsvariablen auf Ihrem Beszel-Hub fest, um die Heartbeat-Überwachung zu aktivieren:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Einstellungen\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Einstellungen gespeichert\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Anmelden\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP-Einstellungen\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sortieren nach\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Startzeit\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Status\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Unterzustand\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Vom System genutzter Swap-Speicher\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap-Nutzung\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Systemlastdurchschnitt im Zeitverlauf\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd-Dienste\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systeme\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systeme können in einer <0>config.yml</0>-Datei im Datenverzeichnis verwaltet werden.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabelle\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Aufgaben\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperatureinheit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturen der Systemsensoren\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test-Heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testbenachrichtigung gesendet\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Der Gesamtstatus ist <0>ok</0>, wenn alle Systeme in Betrieb sind, <1>warn</1>, wenn Warnungen ausgelöst werden, und <2>error</2>, wenn ein System ausgefallen ist.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Melde dich dann im Backend an und setze dein Benutzerkontopasswort in der Benutzertabelle zurück.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Diese Aktion kann nicht rückgängig gemacht werden. Dadurch werden alle aktuellen Datensätze für {name} dauerhaft aus der Datenbank gelöscht.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Dadurch werden alle ausgewählten Datensätze dauerhaft aus der Datenbank gelöscht.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Durchsatz von {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Durchsatz des Root-Dateisystems\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Zeitformat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"An E-Mail(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Raster umschalten\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Darstellung umschalten\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens & Fingerabdrücke\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens ermöglichen es Agents, sich zu verbinden und zu registrieren. Fingerabdrücke sind stabile, eindeutige Identifikatoren für jedes System, die bei der ersten Verbindung gesetzt werden.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens und Fingerabdrücke werden verwendet, um WebSocket-Verbindungen zum Hub zu authentifizieren.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Gesamt\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Empfangene Gesamtdatenmenge je Schnittstelle \"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Gesendete Gesamtdatenmenge je Schnittstelle\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Gesamt: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Ausgelöst von\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Trigger\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Löst aus, wenn der Lastdurchschnitt der letzten Minute einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Löst aus, wenn ein Sensor einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Löst aus, wenn der Batterieladestand unter einen Schwellenwert fällt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Löst aus, wenn die kombinierte Up- und Downloadrate einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Löst aus, wenn die CPU-Auslastung einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Löst aus, wenn die GPU-Auslastung einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Löst aus, wenn die Arbeitsspeichernutzung einen Schwellenwert überschreitet\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Löst aus, wenn der Status zwischen online und offline wechselt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Typ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Unit-Datei\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Einheiten\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universeller Token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Unbekannt\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Unbegrenzt\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Aktiv\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Aktiv ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Aktualisieren\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Aktualisiert\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Alle 10 Minuten aktualisiert.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Hochladen\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Betriebszeit\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Nutzung\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Nutzung der Root-Partition\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Verwendet\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Benutzer\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Wert\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Ansicht\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Mehr anzeigen\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Sieh dir die neusten 200 Alarme an.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Sichtbare Spalten\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Warten auf genügend Datensätze zur Anzeige\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Möchtest du uns helfen, unsere Übersetzungen noch besser zu machen? Schau dir <0>Crowdin</0> für weitere Details an.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Möchte\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Warnung (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Warnschwellen\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push-Benachrichtigungen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Wenn aktiviert, ermöglicht dieser Token Agenten die Selbstregistrierung ohne vorherige Systemerstellung.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Bei Verwendung von POST enthält jeder Heartbeat eine JSON-Payload mit einer Zusammenfassung des Systemstatus, einer Liste der ausgefallenen Systeme und ausgelösten Warnungen.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows-Befehl\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Schreiben\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML-Konfiguration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML-Konfiguration\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Deine Benutzereinstellungen wurden aktualisiert.\"\n"
  },
  {
    "path": "internal/site/src/locales/en/en.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: en\\n\"\n\"Project-Id-Version: \\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: \\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: \\n\"\n\"Plural-Forms: \\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} of {1} row(s) selected.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# core} other {# cores}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} day} other {{countString} days}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# thread} other {# threads}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 hour\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minute\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 week\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 hours\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 hours\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 days\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Actions\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Active\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Active Alerts\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Active state\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Add {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Add <0>System</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Add system\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Add URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Adjust display options for charts.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Adjust the width of the main layout\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"After\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Alert History\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alerts\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"All Containers\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"All Systems\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Are you sure you want to delete {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Are you sure?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatic copy requires a secure context.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Average\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Average CPU utilization of containers\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Average drops below <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Average exceeds <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Average power consumption of GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Average system-wide CPU utilization\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Average utilization of {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Average utilization of GPU engines\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Backups\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandwidth\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Battery\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Became active\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Became inactive\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Before\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binary\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Boot state\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffers\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Can reload\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Can start\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Can stop\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Cancel\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Capabilities\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capacity\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Caution - potential data loss\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Change display units for metrics.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Change general application options.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Charge\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Charging\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Chart options\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Check {email} for a reset link.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Check logs for more details.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Check your monitoring service\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Check your notification service\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Clear\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Click on a container to view more information.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Click on a device to view more information.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Click on a system to view more information.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Click to copy\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Command line instructions\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configure how you receive alert notifications.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Confirm password\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflicts\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Connection is down\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Continue\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Copied to clipboard\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Copy docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Copy docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Copy env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Copy host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Copy Linux command\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Copy name\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Copy text\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Copy YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU Cores\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU Peak\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU time\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU Time Breakdown\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU Usage\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Create\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Create account\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Created\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Critical (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Cumulative Download\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Cumulative Upload\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Current state\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cycles\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Daily\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Default time period\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Delete\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Delete fingerprint\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Description\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detail\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Device\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Discharging\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Disk unit\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Disk Usage\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Disk usage of {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU Usage\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker Memory Usage\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Network I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentation\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Down\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Down ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Download\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Duration\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Edit\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Edit {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Email notifications\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Empty\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"End Time\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Endpoint URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Endpoint URL to ping (required)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Enter email address to reset password\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Enter email address...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Enter your one-time password.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Ephemeral\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Error\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Example:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Exec main PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Exited active\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Expires after one hour or on hub restart.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Export\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Export configuration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Export your current systems configuration.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Failed\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Failed Attributes:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Failed to authenticate\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Failed to save settings\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Failed to send heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Failed to send test notification\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Failed to update alert\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Failed: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filter...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingerprint\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Forgot password?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD command\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Full\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"General\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU Engines\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU Power Draw\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU Usage\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Grid\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Health\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat Monitoring\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat sent successfully\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew command\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP Method\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP method: POST, GET, or HEAD (default: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Idle\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"If you've lost the password to your admin account, you may reset it using the following command.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Image\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inactive\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Invalid email address.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Language\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Layout\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layout width\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Lifecycle\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Load Average\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Load Average 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Load Average 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Load Average 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Load Avg\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Load state\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Loading...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Log Out\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Login\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Login attempt failed\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logs\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Main PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Manage display and notification preferences.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Manual setup instructions\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memory\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Memory limit\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Memory Peak\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Memory Usage\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Memory usage of docker containers\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Name\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Net\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Network traffic of docker containers\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Network traffic of public interfaces\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Network unit\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"No\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"No results found.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"No results.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"No S.M.A.R.T. attributes available for this device.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"No systems found.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notifications\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC support\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"One-time\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"One-time password\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Open menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Or continue with\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Other\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Overwrite existing alerts\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Page\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Page {0} of {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Pages / Settings\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Password\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Password must be at least 8 characters.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Password must be less than 72 bytes.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Password reset request received\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Past\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pause\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Paused\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Paused ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Payload format\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Per-core average utilization\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Percentage of time spent in each state\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistence\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Please check logs for more details.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Please check your credentials and try again\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Please create an admin account\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Please enable pop-ups for this site\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Please log in again\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Please see <0>the documentation</0> for instructions.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Please sign in to your account\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Power On\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Precise utilization at the recorded time\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Preferred Language\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Process started\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Public Key\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Quiet Hours\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Read\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Received\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Refresh\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relationships\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Request a one-time password\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Request OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Required by\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Requires\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Reset Password\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Resolved\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Restarts\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Resume\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Rotate token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Rows per page\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Runtime Metrics\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Details\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Self-Test\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Save address using enter key or comma. Leave blank to disable email notifications.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Save Settings\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Save system\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Saved in the database and does not expire until you disable it.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Schedule\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Schedule quiet hours where notifications will not be sent.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Search\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Search for systems or settings...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Seconds between pings (default: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"See <0>notification settings</0> to configure how you receive alerts.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Select {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Send a single heartbeat ping to verify your endpoint is working.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Send test heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Sent\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serial Number\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Service Details\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Set percentage thresholds for meter colors.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Settings\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Settings saved\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Sign in\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP settings\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sort By\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Start Time\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"State\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Sub State\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap space used by the system\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap Usage\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"System load averages over time\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd Services\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systems\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Table\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tasks\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperature\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperature unit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperatures of system sensors\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Test notification sent\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Then log into the backend and reset your user account password in the users table.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"This will permanently delete all selected records from the database.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Throughput of {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Throughput of root filesystem\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Time format\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"To email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Toggle grid\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Toggle theme\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens & Fingerprints\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Total data received for each interface\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Total data sent for each interface\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Total: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Triggered by\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Triggers\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Triggers when 1 minute load average exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Triggers when 15 minute load average exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Triggers when 5 minute load average exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Triggers when any sensor exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Triggers when battery charge drops below a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Triggers when combined up/down exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Triggers when CPU usage exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Triggers when GPU usage exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Triggers when memory usage exceeds a threshold\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Triggers when status switches between up and down\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Triggers when usage of any disk exceeds a threshold\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Type\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Unit file\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Unit preferences\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universal token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Unknown\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Unlimited\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Up\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Up ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Update\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Updated\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Updated every 10 minutes.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Upload\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Usage\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Usage of root partition\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Used\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Users\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Value\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"View\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"View more\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"View your 200 most recent alerts.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Visible Fields\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Waiting for enough records to display\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Wants\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Warning (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Warning thresholds\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push notifications\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"When enabled, this token allows agents to self-register without prior system creation.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows command\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Write\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML Config\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Configuration\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Yes\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Your user settings have been updated.\"\n"
  },
  {
    "path": "internal/site/src/locales/es/es.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: es\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Spanish\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: es-ES\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} de {1} fila(s) seleccionada(s).\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# núcleo} other {# núcleos}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} día} other {{countString} días}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} hora} other {{countString} horas}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuto} other {{countString} minutos}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# hilo} other {# hilos}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 hora\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuto\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 semana\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 horas\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 horas\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 días\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Acciones\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Activo\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Alertas activas\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Estado activo\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Agregar {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Agregar <0>sistema</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Agregar sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Agregar URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Ajustar las opciones de visualización para los gráficos.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Ajustar el ancho del diseño principal\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Administrador\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Después\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Después de configurar las variables de entorno, reinicie su hub Beszel para que los cambios surtan efecto.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agente\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Historial de alertas\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alertas\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Todos los contenedores\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Todos los sistemas\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"¿Estás seguro de que deseas eliminar {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"¿Estás seguro?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"La copia automática requiere un contexto seguro.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Promedio\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Utilización promedio de CPU de los contenedores\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"El promedio cae por debajo de <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"El promedio excede <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Consumo de energía promedio de GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Utilización promedio de CPU del sistema\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Uso promedio de {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Utilización promedio de motores GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Copias de seguridad\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Ancho de banda\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batería\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Se activó\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Se desactivó\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Antes\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Por debajo de {0}{1} en el último {2, plural, one {# minuto} other {# minutos}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel admite OpenID Connect y muchos proveedores de autenticación OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel utiliza <0>Shoutrrr</0> para integrarse con servicios populares de notificación.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binario\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Estado de arranque\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (kB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Caché / Buffers\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Puede recargarse\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Puede iniciarse\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Puede detenerse\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Capacidades\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capacidad\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Precaución - posible pérdida de datos\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Cambiar las unidades de visualización de las métricas.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Cambiar las opciones generales de la aplicación.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Carga\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Cargando\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Opciones de gráficos\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Revisa {email} para un enlace de restablecimiento.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Revisa los registros para más detalles.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Compruebe su servicio de monitorización\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Verifica tu servicio de notificaciones\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Limpiar\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Haz clic en un contenedor para ver más información.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Haz clic en un dispositivo para ver más información.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Haz clic en un sistema para ver más información.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Haz clic para copiar\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instrucciones de línea de comandos\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configura cómo recibe las notificaciones de alertas.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Confirmar contraseña\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflictos\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"La conexión está caída\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Continuar\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Copiado al portapapeles\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Copiar docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Copiar docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Copiar env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Copiar host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Copiar comando de Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Copiar nombre\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Copiar texto\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Copia el comando de instalación del agente a continuación, o registra agentes automáticamente con un <0>token universal</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Copia el contenido del<0>docker-compose.yml</0> para el agente a continuación, o registra agentes automáticamente con un <1>token universal</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Copiar YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Núcleos de CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Pico de CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Tiempo de CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Desglose de tiempo de CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Uso de CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Crear\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Crear cuenta\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Creada\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Crítico (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Descarga acumulada\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Carga acumulada\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Estado actual\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Ciclos\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Diariamente\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Periodo de tiempo predeterminado\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Eliminar\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Eliminar huella digital\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Descripción\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detalle\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Dispositivo\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Descargando\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"E/S de Disco\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Unidad de disco\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Uso de disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Uso de disco de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Uso de CPU de Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Uso de memoria de Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"E/S de red de Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentación\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Caído\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Caído ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Descargar\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Duración\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Editar\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Editar {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Correo electrónico\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Notificaciones por correo\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Vacía\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Hora de finalización\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL del punto de conexión\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL del punto de conexión para ping (obligatorio)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Ingresa la dirección de correo electrónico para restablecer la contraseña\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Ingresa dirección de correo...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Ingrese su contraseña de un solo uso.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Efímero\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Error\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Ejemplo:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Excede {0}{1} en el último {2, plural, one {# minuto} other {# minutos}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID principal de ejecución\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Los sistemas existentes no definidos en <0>config.yml</0> serán eliminados. Por favor, haz copias de seguridad regularmente.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Salió activo\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Expira después de una hora o al reiniciar el hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportar\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Exportar configuración\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exporta la configuración actual de sus sistemas.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Fallido\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Atributos fallidos:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Error al autenticar\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Error al guardar la configuración\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Error al enviar el latido (heartbeat)\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Error al enviar la notificación de prueba\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Error al actualizar la alerta\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Fallidos: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtrar...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Huella dactilar\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"¿Olvidaste tu contraseña?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Comando FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Llena\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"General\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Motores GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Consumo de energía de la GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Uso de GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Cuadrícula\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Estado\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitorización de Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Latido enviado con éxito\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Comando Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Servidor / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Método HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Método HTTP: POST, GET o HEAD (predeterminado: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inactiva\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Si has perdido la contraseña de tu cuenta de administrador, puedes restablecerla usando el siguiente comando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Imagen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inactivo\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervalo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Dirección de correo electrónico no válida.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Idioma\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Diseño\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Ancho del diseño\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Ciclo de vida\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"límite\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Carga media\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Carga media 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Carga media 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Carga media 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Carga media\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Estado de carga\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Cargando...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Cerrar sesión\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Iniciar sesión\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Intento de inicio de sesión fallido\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Registros\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"¿Buscas dónde crear alertas? Haz clic en los iconos de campana <0/> en la tabla de sistemas.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID principal\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Administrar preferencias de visualización y notificaciones.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Instrucciones manuales de configuración\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Máx. 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memoria\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Límite de memoria\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Pico de memoria\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Uso de memoria\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Uso de memoria de los contenedores Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modelo\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nombre\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Red\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Tráfico de red de los contenedores Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Tráfico de red de interfaces públicas\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Unidad de red\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"No\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"No se encontraron resultados.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Sin resultados.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"No hay atributos S.M.A.R.T. disponibles para este dispositivo.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"No se encontraron sistemas.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notificaciones\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Soporte para OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"En cada reinicio, los sistemas en la base de datos se actualizarán para coincidir con los sistemas definidos en el archivo.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Una vez\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Contraseña de un solo uso\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Abrir menú\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"O continuar con\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Otro\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Sobrescribir alertas existentes\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Página\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Página {0} de {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Páginas / Configuraciones\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Contraseña\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"La contraseña debe tener al menos 8 caracteres.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"La contraseña debe ser menor de 72 bytes.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Solicitud de restablecimiento de contraseña recibida\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Pasado\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pausar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pausado\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pausado ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Formato de carga útil (payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Uso promedio por núcleo\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Porcentaje de tiempo dedicado a cada estado\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanente\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistencia\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Por favor, <0>configura un servidor SMTP</0> para asegurar que las alertas sean entregadas.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Por favor, revisa los registros para más detalles.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Por favor, verifica tus credenciales e inténtalo de nuevo\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Por favor, crea una cuenta de administrador\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Por favor, habilita las ventanas emergentes para este sitio\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Por favor, inicia sesión de nuevo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Por favor, consulta <0>la documentación</0> para obtener instrucciones.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Por favor, inicia sesión en tu cuenta\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Puerto\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Encendido\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Utilización precisa en el momento registrado\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Idioma preferido\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proceso iniciado\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Clave pública\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Horas de silencio\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lectura\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Recibido\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Actualizar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relaciones\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Solicitar contraseña de un solo uso\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Solicitar OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Requerido por\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Requiere\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Restablecer contraseña\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Resuelto\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Reinicios\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Reanudar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Raíz\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Rotar token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Filas por página\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Métricas de tiempo de ejecución\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Detalles S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Autoprueba S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Guarda la dirección usando la tecla enter o coma. Deja en blanco para desactivar las notificaciones por correo.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Guardar configuración\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Guardar sistema\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Guardado en la base de datos y no expira hasta que lo desactives.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Programar\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Programe horas de silencio donde no se enviarán notificaciones, como durante períodos de mantenimiento.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Programe horas de silencio donde no se enviarán notificaciones.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Buscar\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Buscar sistemas o configuraciones...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Segundos entre pings (predeterminado: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Consulta la <0>configuración de notificaciones</0> para configurar cómo recibes alertas.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Seleccionar {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Envíe un único ping de latido para verificar que su punto de conexión funciona.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Envíe pings salientes periódicos a un servicio de monitorización externo para que pueda supervisar Beszel sin exponerlo a internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Enviar latido de prueba\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Enviado\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Número de serie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Detalles del servicio\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Servicios\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Establecer umbrales de porcentaje para los colores de los medidores.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Configure las siguientes variables de entorno en su hub Beszel para habilitar la monitorización de latidos:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Configuración\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Configuración guardada\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Iniciar sesión\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Configuración SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Ordenar por\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Hora de inicio\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Estado\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Estado\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Subestado\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Espacio de swap utilizado por el sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Uso de swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Promedios de carga del sistema a lo largo del tiempo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Servicios de systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistemas\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Los sistemas pueden ser gestionados en un archivo <0>config.yml</0> dentro de tu directorio de datos.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabla\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tareas\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Unidad de temperatura\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturas de los sensores del sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Probar <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Probar latido\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Notificación de prueba enviada\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"El estado general es <0>ok</0> cuando todos los sistemas están activos, <1>warn</1> cuando se activan alertas y <2>error</2> cuando algún sistema está caído.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Luego inicia sesión en el backend y restablece la contraseña de tu cuenta de usuario en la tabla de usuarios.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Esta acción no se puede deshacer. Esto eliminará permanentemente todos los registros actuales de {name} de la base de datos.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Esto eliminará permanentemente todos los registros seleccionados de la base de datos.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Rendimiento de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Rendimiento del sistema de archivos raíz\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Formato de hora\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"A correo(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Alternar cuadrícula\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Alternar tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens y huellas digitales\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Los tokens permiten que los agentes se conecten y registren. Las huellas digitales son identificadores estables únicos para cada sistema, establecidos en la primera conexión.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Los tokens y las huellas digitales se utilizan para autenticar las conexiones WebSocket al hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Datos totales recibidos por cada interfaz\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Datos totales enviados por cada interfaz\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Total: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Activado por\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Activadores\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Se activa cuando la carga media de 1 minuto supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Se activa cuando la carga media de 15 minutos supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Se activa cuando la carga media de 5 minutos supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Se activa cuando cualquier sensor supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Se activa cuando la carga de la batería baja de un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Se activa cuando la suma de subida/bajada supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Se activa cuando el uso de CPU supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Se activa cuando el uso de GPU supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Se activa cuando el uso de memoria supera un umbral\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Se activa cuando el estado cambia entre activo e inactivo\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Se activa cuando el uso de cualquier disco supera un umbral\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Tipo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Archivo de unidad\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Preferencias de unidad\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token universal\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Desconocida\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Ilimitado\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Activo\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Activo ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Actualizar\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Actualizado\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Actualizado cada 10 minutos.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Cargar\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Uso\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Uso de la partición raíz\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Usado\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Usuarios\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Valor\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Vista\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Ver más\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Ver tus 200 alertas más recientes.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Columnas visibles\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Esperando suficientes registros para mostrar\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"¿Quieres ayudar a mejorar nuestras traducciones? Consulta <0>Crowdin</0> para más detalles.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Desea\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Advertencia (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Umbrales de advertencia\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Notificaciones Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Cuando está habilitado, este token permite a los agentes registrarse automáticamente sin creación previa del sistema.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Al usar POST, cada latido incluye una carga útil JSON con un resumen del estado del sistema, una lista de sistemas caídos y alertas activadas.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Comando Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Escritura\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Configuración YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Configuración YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Sí\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Tu configuración de usuario ha sido actualizada.\"\n"
  },
  {
    "path": "internal/site/src/locales/fa/fa.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fa\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Persian\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: fa\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} از {1} ردیف انتخاب شده است.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# هسته} other {# هسته}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} روز} other {{countString} روز}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} ساعت} other {{countString} ساعت}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} دقیقه} few {{countString} دقیقه} many {{countString} دقیقه} other {{countString} دقیقه}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# رشته} other {# رشته}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"۱ ساعت\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"۱ دقیقه\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 دقیقه\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"۱ هفته\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"۱۲ ساعت\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"۱۵ دقیقه\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"۲۴ ساعت\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"۳۰ روز\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"۵ دقیقه\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"عملیات\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"فعال\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \" هشدارهای فعال\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"وضعیت فعال\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"افزودن {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"افزودن <0>سیستم</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"افزودن سیستم\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"افزودن آدرس اینترنتی\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"تنظیم گزینه‌های نمایش برای نمودارها.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"تنظیم عرض چیدمان اصلی\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"مدیر\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"بعد از\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"پس از تنظیم متغیرهای محیطی، هاب Beszel خود را مجدداً راه اندازی کنید تا تغییرات اعمال شوند.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"عامل\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"تاریخچه هشدارها\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"هشدارها\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"همه کانتینرها\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"همه سیستم‌ها\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"آیا مطمئن هستید که می‌خواهید {name} را حذف کنید؟\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"آیا مطمئن هستید؟\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"کپی خودکار نیاز به یک زمینه امن دارد.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"میانگین\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"میانگین استفاده از CPU کانتینرها\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"میانگین به زیر <0>{value}{0}</0> می‌افتد\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"میانگین از <0>{value}{0}</0> فراتر رفته است\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"میانگین مصرف برق پردازنده‌های گرافیکی\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"میانگین استفاده از CPU در کل سیستم\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"میانگین استفاده از {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"میانگین استفاده از موتورهای GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"پشتیبان‌گیری‌ها\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"پهنای باند\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"باتری\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"باتری\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"فعال شد\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"غیرفعال شد\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"قبل از\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"زیر {0}{1} در آخرین {2, plural, one {# دقیقه} other {# دقیقه}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"بِزل از OpenID Connect و بسیاری از ارائه‌دهندگان احراز هویت OAuth2 پشتیبانی می‌کند.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"بِزل از <0>Shoutrrr</0> برای ادغام با سرویس‌های اطلاع‌رسانی محبوب استفاده می‌کند.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"دودویی\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"بیت (کیلوبیت بر ثانیه، مگابیت بر ثانیه، گیگابیت بر ثانیه)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"وضعیت بوت\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"بایت (کیلوبایت بر ثانیه، مگابایت بر ثانیه، گیگابایت بر ثانیه)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"حافظه پنهان / بافرها\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"می‌تواند بارگذاری مجدد شود\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"می‌تواند شروع شود\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"می‌تواند متوقف شود\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"لغو\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"قابلیت‌ها\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"ظرفیت\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"احتیاط - احتمال از دست رفتن داده‌ها\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"سلسیوس (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"تغییر واحدهای نمایش برای معیارها.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"تغییر گزینه‌های کلی برنامه.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"شارژ\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"در حال شارژ\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"گزینه‌های نمودار\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"ایمیل {email} خود را برای لینک بازنشانی بررسی کنید.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"برای جزئیات بیشتر، لاگ‌ها را بررسی کنید.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"سرویس نظارتی خود را بررسی کنید\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"سرویس اطلاع‌رسانی خود را بررسی کنید\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"پاک کردن\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"برای مشاهده اطلاعات بیشتر روی کانتینر کلیک کنید.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"برای مشاهده اطلاعات بیشتر روی دستگاه کلیک کنید.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"برای مشاهده اطلاعات بیشتر روی یک سیستم کلیک کنید.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"برای کپی کردن کلیک کنید\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"دستورالعمل‌های خط فرمان\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"نحوه دریافت هشدارهای اطلاع‌رسانی را پیکربندی کنید.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"تأیید رمز عبور\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"تعارض‌ها\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"اتصال قطع است\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"ادامه\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"در کلیپ‌بورد کپی شد\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"کپی docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"کپی docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"کپی متغیرهای محیط\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"کپی میزبان\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"کپی دستور لینوکس\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"کپی نام\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"کپی متن\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"دستور نصب عامل زیر را کپی کنید، یا عامل‌ها را به طور خودکار با <0>توکن جهانی</0> ثبت کنید.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"محتوای <0>docker-compose.yml</0> عامل زیر را کپی کنید، یا عامل‌ها را به طور خودکار با <1>توکن جهانی</1> ثبت کنید.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"کپی YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"پردازنده\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"هسته‌های CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"حداکثر CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"زمان CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"تجزیه زمان CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"میزان استفاده از پردازنده\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"ایجاد\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"ایجاد حساب کاربری\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"ایجاد شده\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"بحرانی (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"دانلود تجمعی\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"آپلود تجمعی\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"وضعیت فعلی\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"چرخه‌ها\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"روزانه\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"بازه زمانی پیش‌فرض\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"حذف\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"حذف اثر انگشت\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"توضیحات\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"جزئیات\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"دستگاه\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"در حال تخلیه\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"دیسک\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"ورودی/خروجی دیسک\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"واحد دیسک\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"میزان استفاده از دیسک\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"میزان استفاده از دیسک {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"میزان استفاده از CPU داکر\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"میزان استفاده از حافظه داکر\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"ورودی/خروجی شبکه داکر\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"مستندات\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"قطع\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"قطع ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"دانلود\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"مدت زمان\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"ویرایش\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"ویرایش {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"ایمیل\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"اعلان‌های ایمیلی\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"خالی\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"زمان پایان\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL نقطه پایانی\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL نقطه پایانی برای پینگ (الزامی)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"آدرس ایمیل را برای بازنشانی رمز عبور وارد کنید\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"آدرس ایمیل را وارد کنید...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"رمز عبور یک‌بار مصرف خود را وارد کنید.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"گذرا\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"خطا\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"مثال:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"در {2, plural, one {# دقیقه} other {# دقیقه}} گذشته از {0}{1} بیشتر است\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID اصلی اجرایی\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"سیستم‌های موجود که در <0>config.yml</0> تعریف نشده‌اند حذف خواهند شد. لطفاً به طور منظم پشتیبان‌گیری کنید.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"خروج فعال\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"پس از یک ساعت یا راه‌اندازی مجدد هاب منقضی می‌شود.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"خروجی گرفتن\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"خارج کردن پیکربندی\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"پیکربندی سیستم‌های فعلی خود را خارج کنید.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"فارنهایت (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"ناموفق\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"ویژگی‌های ناموفق:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"احراز هویت ناموفق بود\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"ذخیره تنظیمات ناموفق بود\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"ارسال ضربان قلب ناموفق بود\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"ارسال اعلان آزمایشی ناموفق بود\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"به‌روزرسانی هشدار ناموفق بود\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"ناموفق: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"فیلتر...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"اثر انگشت\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"فرم‌ویر\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"برای <0>{min}</0> {min, plural, one {دقیقه} other {دقیقه}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"رمز عبور را فراموش کرده‌اید؟\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"دستور FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"پر\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"عمومی\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"جهانی\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"موتورهای GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"مصرف برق پردازنده گرافیکی\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"میزان استفاده از GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"جدول\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"سلامتی\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"ضربان قلب\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"نظارت بر ضربان قلب\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"ضربان قلب با موفقیت ارسال شد\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"دستور Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"میزبان / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"متد HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"متد HTTP: POST، GET، یا HEAD (پیش‌فرض: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"بیکار\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"اگر رمز عبور حساب مدیر خود را گم کرده‌اید، می‌توانید آن را با استفاده از دستور زیر بازنشانی کنید.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"تصویر\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"غیرفعال\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"بازه زمانی\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"آدرس ایمیل نامعتبر است.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"زبان\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"طرح‌بندی\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"عرض چیدمان\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"چرخه حیات\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"محدودیت\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"میانگین بار\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"میانگین بار ۱۵ دقیقه\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"میانگین بار ۱ دقیقه\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"میانگین بار ۵ دقیقه\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"میانگین بار\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"وضعیت بارگذاری\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"در حال بارگذاری...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"خروج\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"ورود\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"تلاش برای ورود ناموفق بود\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"لاگ‌ها\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"به دنبال جایی برای ایجاد هشدار هستید؟ روی آیکون‌های زنگ <0/> در جدول سیستم‌ها کلیک کنید.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID اصلی\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"مدیریت تنظیمات نمایش و اعلان‌ها.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"دستورالعمل‌های راه‌اندازی دستی\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"حداکثر ۱ دقیقه\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"حافظه\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"محدودیت حافظه\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"حداکثر حافظه\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"میزان استفاده از حافظه\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"میزان استفاده از حافظه کانتینرهای داکر\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"مدل\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"نام\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"شبکه\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"ترافیک شبکه کانتینرهای داکر\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"ترافیک شبکه رابط‌های عمومی\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"واحد شبکه\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"خیر\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"هیچ نتیجه‌ای یافت نشد.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"نتیجه‌ای یافت نشد.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"هیچ ویژگی S.M.A.R.T برای این دستگاه موجود نیست.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"هیچ سیستمی یافت نشد.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"اعلان‌ها\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"پشتیبانی از OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"در هر بار راه‌اندازی مجدد، سیستم‌های موجود در پایگاه داده با سیستم‌های تعریف شده در فایل مطابقت داده می‌شوند.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"یک‌بار مصرف\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"رمز عبور یک‌بار مصرف\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"باز کردن منو\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"یا ادامه با\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"سایر\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"بازنویسی هشدارهای موجود\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"صفحه\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"صفحه {0} از {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"صفحات / تنظیمات\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"رمز عبور\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"رمز عبور باید حداقل ۸ کاراکتر باشد.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"رمز عبور باید کمتر از ۷۲ بایت باشد.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"درخواست بازنشانی رمز عبور دریافت شد\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"گذشته\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"توقف\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"مکث شده\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"مکث شده ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"فرمت پی‌لود\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"میانگین استفاده در هر هسته\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"درصد زمان صرف شده در هر حالت\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"دائمی\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"ماندگاری\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"لطفاً برای اطمینان از تحویل هشدارها، یک <0>سرور SMTP پیکربندی کنید</0>.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"لطفاً برای جزئیات بیشتر، لاگ‌ها را بررسی کنید.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"لطفاً اعتبارنامه‌های خود را بررسی کرده و دوباره تلاش کنید.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"لطفاً یک حساب مدیر ایجاد کنید\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"لطفاً پنجره‌های بازشو را برای این سایت فعال کنید\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"لطفاً دوباره وارد شوید\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"لطفاً برای دستورالعمل‌ها به <0>مستندات</0> مراجعه کنید.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"لطفاً به حساب کاربری خود وارد شوید\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"پورت\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"روشن کردن\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"میزان دقیق استفاده در زمان ثبت شده\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"زبان ترجیحی\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"فرآیند شروع شد\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"کلید عمومی\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"ساعات آرام\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"خواندن\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"دریافت شد\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"تازه‌سازی\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"روابط\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"درخواست رمز عبور یک‌بار مصرف\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"درخواست OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"مورد نیاز توسط\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"نیازمند\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"بازنشانی رمز عبور\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"حل شده\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"راه‌اندازی مجدد\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"ادامه\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"ریشه\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"چرخش توکن\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"ردیف در هر صفحه\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"معیارهای زمان اجرا\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"جزئیات S.M.A.R.T\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"تست خود S.M.A.R.T\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"آدرس را با استفاده از کلید Enter یا کاما ذخیره کنید. برای غیرفعال کردن اعلان‌های ایمیلی، خالی بگذارید.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"ذخیره تنظیمات\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"ذخیره سیستم\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"در پایگاه داده ذخیره شده و تا زمانی که آن را غیرفعال نکنید، منقضی نمی‌شود.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"برنامه‌ریزی\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"برنامه‌ریزی ساعات آرام که در آن اعلان‌ها ارسال نخواهند شد، مانند در طول دوره‌های تعمیر و نگهداری.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"برنامه‌ریزی ساعات آرام که در آن اعلان‌ها ارسال نخواهند شد.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"جستجو\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"جستجو برای سیستم‌ها یا تنظیمات...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"ثانیه بین پینگ‌ها (پیش‌فرض: ۶۰)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"برای پیکربندی نحوه دریافت هشدارها، به <0>تنظیمات اعلان</0> مراجعه کنید.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"انتخاب {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"یک پینگ ضربان قلب تکی ارسال کنید تا از کارکرد نقطه پایانی خود اطمینان حاصل کنید.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"پینگ‌های خروجی دوره‌ای را به یک سرویس نظارتی خارجی ارسال کنید تا بتوانید Beszel را بدون قرار دادن آن در معرض اینترنت نظارت کنید.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"ارسال ضربان قلب آزمایشی\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"ارسال شد\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"شماره سریال\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"جزئیات سرویس\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"سرویس‌ها\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"آستانه های درصدی را برای رنگ های متر تنظیم کنید.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"متغیرهای محیطی زیر را در هاب Beszel خود تنظیم کنید تا نظارت بر ضربان قلب فعال شود:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"تنظیمات\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"تنظیمات ذخیره شد\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"ورود\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"تنظیمات SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"مرتب‌سازی بر اساس\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"زمان شروع\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"وضعیت\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"وضعیت\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"وضعیت فرعی\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"فضای Swap استفاده شده توسط سیستم\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"میزان استفاده از Swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"سیستم\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"میانگین بار سیستم در طول زمان\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"خدمات Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"سیستم‌ها\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"سیستم‌ها ممکن است در یک فایل <0>config.yml</0> درون دایرکتوری داده شما مدیریت شوند.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"جدول\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"وظایف\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"دما\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"دما\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"واحد دما\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"دمای حسگرهای سیستم\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"تست <0>آدرس اینترنتی</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"تست ضربان قلب\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"اعلان آزمایشی ارسال شد\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"وضعیت کلی زمانی <0>ok</0> است که همه سیستم‌ها بالا باشند، <1>warn</1> زمانی که هشدارها فعال شوند، و <2>error</2> زمانی که هر سیستمی پایین باشد.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"سپس وارد بخش پشتیبان شوید و رمز عبور حساب کاربری خود را در جدول کاربران بازنشانی کنید.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"این عمل قابل برگشت نیست. این کار تمام رکوردهای فعلی {name} را برای همیشه از پایگاه داده حذف خواهد کرد.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"این کار تمام رکوردهای انتخاب شده را برای همیشه از پایگاه داده حذف خواهد کرد.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"توان عملیاتی {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"توان عملیاتی سیستم فایل ریشه\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"فرمت زمان\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"به ایمیل(ها)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"تغییر نمایش جدول\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"تغییر تم\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"توکن\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"توکن‌ها و اثرات انگشت\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"توکن‌ها به عامل‌ها اجازه اتصال و ثبت‌نام می‌دهند. اثرات انگشت شناسه‌های پایدار منحصر به فرد هر سیستم هستند که در اولین اتصال تنظیم می‌شوند.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"توکن‌ها و اثرات انگشت برای احراز هویت اتصالات WebSocket به هاب استفاده می‌شوند.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"کل\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"داده‌های کل دریافت شده برای هر رابط\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"داده‌های کل ارسال شده برای هر رابط\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"کل: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"فعال شده توسط\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"محرک‌ها\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"هنگامی که میانگین بار ۱ دقیقه‌ای از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"هنگامی که میانگین بار ۱۵ دقیقه‌ای از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"هنگامی که میانگین بار ۵ دقیقه‌ای از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"هنگامی که هر حسگری از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"زمانی که شارژ باتری زیر آستانه قرار می‌گیرد، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"هنگامی که مجموع بالا/پایین از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"هنگامی که میزان استفاده از CPU از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"هنگامی که میزان استفاده از GPU از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"هنگامی که میزان استفاده از حافظه از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"هنگامی که وضعیت بین بالا و پایین تغییر می‌کند، فعال می‌شود\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"هنگامی که استفاده از هر دیسکی از یک آستانه فراتر رود، فعال می‌شود\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"نوع\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"فایل واحد\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"تنظیمات واحدها\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"توکن جهانی\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"ناشناخته\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"نامحدود\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"فعال\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"فعال ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"به‌روزرسانی\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"به‌روزرسانی شد\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"هر ۱۰ دقیقه به‌روزرسانی می‌شود.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"آپلود\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"آپتایم\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"میزان استفاده\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"میزان استفاده از پارتیشن ریشه\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"استفاده شده\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"کاربران\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"مقدار\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"مشاهده\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"مشاهده بیشتر\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"۲۰۰ هشدار اخیر خود را مشاهده کنید.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"فیلدهای قابل مشاهده\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"در انتظار رکوردهای کافی برای نمایش\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"می‌خواهید به ما کمک کنید تا ترجمه‌های خود را بهتر کنیم؟ برای جزئیات بیشتر به <0>Crowdin</0> مراجعه کنید.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"می‌خواهد\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"هشدار (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"آستانه های هشدار\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"اعلان‌های Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"هنگامی که فعال باشد، این توکن به عوامل اجازه می‌دهد بدون ایجاد سیستم قبلی، خود را ثبت کنند.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"هنگام استفاده از POST، هر ضربان قلب شامل یک پی‌لود JSON با خلاصه وضعیت سیستم، لیست سیستم‌های پایین و هشدارهای فعال شده است.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"دستور Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"نوشتن\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"پیکربندی YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"پیکربندی YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"بله\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"تنظیمات کاربری شما به‌روزرسانی شد.\"\n"
  },
  {
    "path": "internal/site/src/locales/fr/fr.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: fr\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: French\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: fr\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} sur {1} ligne(s) sélectionnée(s).\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# cœur} other {# cœurs}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} jour} other {{countString} jours}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} heure} other {{countString} heures}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minute} other {{countString} minutes}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# fil} other {# fils}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 heure\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minute\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 semaine\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 heures\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 heures\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 jours\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Actions\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Active\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Alertes actives\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"État actif\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Ajouter {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Ajouter <0>un Système</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Ajouter un système\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Ajouter l’URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Ajuster les options d'affichage pour les graphiques.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Ajuster la largeur de la mise en page principale\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Après\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Après avoir défini les variables d'environnement, redémarrez votre hub Beszel pour que les changements prennent effet.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Historique des alertes\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alertes\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Tous les conteneurs\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Tous les systèmes\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Êtes-vous sûr de vouloir supprimer {name} ?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Êtes-vous sûr ?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"La copie automatique nécessite un contexte sécurisé.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Moyenne\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Utilisation moyenne du CPU des conteneurs\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"La moyenne descend en dessous de <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"La moyenne dépasse <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Consommation d'énergie moyenne des GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Utilisation moyenne du CPU à l'échelle du système\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Utilisation moyenne de {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Utilisation moyenne des moteurs GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Sauvegardes\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bande passante\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batterie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Devenu actif\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Devenu inactif\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Avant\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Inférieur à {0}{1} dans {2, plural, one {la dernière # minute} other {les dernières # minutes}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel prend en charge OpenID Connect et de nombreux fournisseurs d'authentification OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel utilise <0>Shoutrrr</0> pour s'intégrer aux services de notification populaires.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binaire\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"État de démarrage\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Tampons\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Peut recharger\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Peut démarrer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Peut arrêter\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Annuler\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Capacités\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capacité\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Attention - perte de données potentielle\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Ajuster les unités d'affichage pour les métriques.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Modifier les options générales de l'application.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Charge\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"En charge\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Options de graphique\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Vérifiez {email} pour un lien de réinitialisation.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Vérifiez les journaux pour plus de détails.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Vérifiez votre service de surveillance\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Vérifiez votre service de notification\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Effacer\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Cliquez sur un conteneur pour voir plus d'informations.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Cliquez sur un appareil pour voir plus d'informations.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Cliquez sur un système pour voir plus d'informations.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Cliquez pour copier\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instructions en ligne de commande\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configurez comment vous recevez les notifications d'alerte.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Confirmer le mot de passe\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflits\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Connexion interrompue\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Continuer\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Copié dans le presse-papiers\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Copier docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Copier docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Copier env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Copier l'hôte\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Copier la commande Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Copier le nom\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Copier le texte\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Copiez la commande d'installation de l'agent ci-dessous, ou enregistrez les agents automatiquement avec un <0>token universel</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Copiez le contenu du<0>docker-compose.yml</0> pour l'agent ci-dessous, ou enregistrez les agents automatiquement avec un <1>token universel</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Copier YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Cœurs CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Pic CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Temps CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Répartition du temps CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Utilisation du CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Créer\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Créer un compte\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Date de création\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Critique (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Téléchargement cumulatif\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Téléversement cumulatif\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"État actuel\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cycles\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Quotidien\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Période par défaut\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Supprimer\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Supprimer l'empreinte\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Description\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Détail\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Appareil\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"En décharge\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disque\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Entrée/Sortie disque\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Unité disque\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Utilisation du disque\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Utilisation du disque de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Utilisation du CPU Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Utilisation de la mémoire Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Entrée/Sortie réseau Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentation\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Hors ligne\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Injoignable ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Télécharger\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Durée\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Éditer\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Modifier {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Notifications par email\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Vide\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Heure de fin\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL du point de terminaison\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL du point de terminaison à pinguer (requis)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Entrez l'adresse email pour réinitialiser le mot de passe\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Entrez l'adresse email...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Entrez votre mot de passe à usage unique.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Éphémère\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Erreur\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Exemple :\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Dépasse {0}{1} dans {2, plural, one {la dernière # minute} other {les dernières # minutes}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID principal d'exécution\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Les systèmes existants non définis dans <0>config.yml</0> seront supprimés. Veuillez faire des sauvegardes régulières.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Sorti actif\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Expire après une heure ou au redémarrage du hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exporter\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Exporter la configuration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exportez la configuration actuelle de vos systèmes.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Échoué\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Attributs défaillants :\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Échec de l'authentification\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Échec de l'enregistrement des paramètres\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Échec de l'envoi du battement de cœur (heartbeat)\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Échec de l'envoi de la notification de test\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Échec de la mise à jour de l'alerte\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Échec : {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtrer...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Empreinte\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Micrologiciel\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Pendant <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Mot de passe oublié ?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Commande FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Pleine\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Général\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Moteurs GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Consommation du GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Utilisation GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Grille\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Santé\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Surveillance Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Battement de cœur envoyé avec succès\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Commande Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Hôte / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Méthode HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Méthode HTTP : POST, GET ou HEAD (par défaut : POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inactive\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Si vous avez perdu le mot de passe de votre compte administrateur, vous pouvez le réinitialiser en utilisant la commande suivante.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Image\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inactif\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervalle\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Adresse email invalide.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Langue\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Disposition\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Largeur de la mise en page\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Cycle de vie\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limite\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Charge moyenne\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Charge moyenne 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Charge moyenne 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Charge moyenne 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Charge moy.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"État de charge\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Chargement...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Déconnexion\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Connexion\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Échec de la tentative de connexion\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Journaux\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Vous cherchez plutôt où créer des alertes ? Cliquez sur les icônes de cloche <0/> dans le tableau des systèmes.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID principal\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Gérer les préférences d'affichage et de notification.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Guide pour une installation manuelle\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Mémoire\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Limite mémoire\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Pic mémoire\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Utilisation de la mémoire\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Utilisation de la mémoire des conteneurs Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modèle\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nom\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Rés\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Trafic réseau des conteneurs Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Trafic réseau des interfaces publiques\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Unité réseau\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Non\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Aucun résultat trouvé.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Aucun résultat.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Aucun attribut S.M.A.R.T. disponible pour cet appareil.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Aucun système trouvé.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notifications\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Support OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"À chaque redémarrage, les systèmes dans la base de données seront mis à jour pour correspondre aux systèmes définis dans le fichier.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Unique\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Mot de passe à usage unique\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Ouvrir le menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Ou continuer avec\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Autre\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Écraser les alertes existantes\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Page\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Page {0} sur {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Pages / Paramètres\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Mot de passe\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Le mot de passe doit contenir au moins 8 caractères.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Le mot de passe doit être inférieur à 72 Octets.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Demande de réinitialisation du mot de passe reçue\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Passé\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pause\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"En pause\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Mis en pause ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Format de la charge utile\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Utilisation moyenne par cœur\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Pourcentage de temps passé dans chaque état\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistance\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Veuillez <0>configurer un serveur SMTP</0> pour garantir la livraison des alertes.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Veuillez vérifier les journaux pour plus de détails.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Veuillez vérifier vos identifiants et réessayer\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Veuillez créer un compte administrateur\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Veuillez activer les pop-ups pour ce site\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Veuillez vous reconnecter\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Veuillez consulter <0>la documentation</0> pour les instructions.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Veuillez vous connecter à votre compte\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Allumage\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Utilisation précise au moment enregistré\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Langue préférée\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Processus démarré\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Clé publique\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Heures calmes\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lecture\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Reçu\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Actualiser\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relations\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Demander un mot de passe à usage unique\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Demander OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Requis par\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Requiert\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Réinitialiser le mot de passe\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Résolu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Redémarrages\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Reprendre\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Racine\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Faire tourner le token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Lignes par page\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Métriques d'exécution\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Détails S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Auto-test S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Enregistrez l'adresse en utilisant la touche Entrée ou la virgule. Laissez vide pour désactiver les notifications par email.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Enregistrer les paramètres\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Sauvegarder le système\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Enregistré dans la base de données et n'expire pas tant que vous ne le désactivez pas.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Programmer\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Programmez des heures calmes où les notifications ne seront pas envoyées, par exemple pendant les périodes de maintenance.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Programmez des heures calmes où les notifications ne seront pas envoyées.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Recherche\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Rechercher des systèmes ou des paramètres...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Secondes entre les pings (par défaut : 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Voir les <0>paramètres de notification</0> pour configurer comment vous recevez les alertes.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Sélectionner {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Envoyez un seul ping heartbeat pour vérifier que votre point de terminaison fonctionne.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Envoyez des pings sortants périodiques vers un service de surveillance externe afin de pouvoir surveiller Beszel sans l'exposer à Internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Envoyer un heartbeat de test\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Envoyé\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Numéro de série\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Détails du service\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Définir des seuils de pourcentage pour les couleurs des compteurs.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Définissez les variables d'environnement suivantes sur votre hub Beszel pour activer la surveillance du heartbeat :\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Paramètres\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Paramètres enregistrés\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Se connecter\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Paramètres SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Trier par\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Heure de début\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"État\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Statut\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Sous-état\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Espace Swap utilisé par le système\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Utilisation du swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Système\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Charges moyennes du système dans le temps\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Services systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systèmes\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Les systèmes peuvent être gérés dans un fichier <0>config.yml</0> à l'intérieur de votre répertoire de données.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tableau\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tâches\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp.\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Température\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Unité de température\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Températures des capteurs du système\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Tester <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Tester le heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Notification de test envoyée\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"L'état général est <0>ok</0> quand tous les systèmes sont opérationnels, <1>warn</1> quand des alertes sont déclenchées, et <2>error</2> quand un système est en panne.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Ensuite, connectez-vous au backend et réinitialisez le mot de passe de votre compte utilisateur dans la table des utilisateurs.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Cette action ne peut pas être annulée. Cela supprimera définitivement tous les enregistrements actuels pour {name} de la base de données.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Ceci supprimera définitivement tous les enregistrements sélectionnés de la base de données.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Débit de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Débit du système de fichiers racine\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Format d'heure\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Aux email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Basculer la grille\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Changer le thème\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens et Empreintes\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Les tokens permettent aux agents de se connecter et de s'enregistrer. Les empreintes sont des identifiants stables uniques à chaque système, définis lors de la première connexion.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Les tokens et les empreintes sont utilisés pour authentifier les connexions WebSocket vers le hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Données totales reçues pour chaque interface\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Données totales envoyées pour chaque interface\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Total : {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Déclenché par\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Déclencheurs\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Se déclenche lorsque la charge moyenne sur 1 minute dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Se déclenche lorsque la charge moyenne sur 15 minutes dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Se déclenche lorsque la charge moyenne sur 5 minutes dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Déclenchement lorsque tout capteur dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Déclenchement lorsque la charge de la batterie descend en dessous d'un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Déclenchement lorsque le montant/descendant combinée dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Déclenchement lorsque l'utilisation du CPU dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Déclenchement lorsque l'utilisation du GPU dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Déclenchement lorsque l'utilisation de la mémoire dépasse un seuil\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Se déclenche lorsque le statut passe de \\\"Joignable\\\" à \\\"Injoignable\\\"\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Déclenchement lorsque l'utilisation de tout disque dépasse un seuil\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Type\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Fichier unité\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Préférences des unités\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token universel\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Inconnue\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Illimité\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Joignable\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Joignable ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Mettre à jour\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Mis à jour\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Mis à jour toutes les 10 minutes.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Téléverser\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Utilisation\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Utilisation de la partition racine\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Utilisé\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Utilisateurs\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Valeur\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Vue\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Voir plus\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Voir vos 200 dernières alertes.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Colonnes visibles\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"En attente de suffisamment d'enregistrements à afficher\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Vous voulez nous aider à améliorer nos traductions ? Consultez <0>Crowdin</0> pour plus de détails.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Souhaite\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Avertissement (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Seuils d'avertissement\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Notifications Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Lorsqu'il est activé, ce jeton permet aux agents de s'enregistrer automatiquement sans création préalable du système.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"En utilisant POST, chaque heartbeat inclut une charge utile JSON avec un résumé de l'état du sistema, la liste des systèmes en panne et les alertes déclenchées.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Commande Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Écriture\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Configuration YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Configuration YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Oui\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Vos paramètres utilisateur ont été mis à jour.\"\n"
  },
  {
    "path": "internal/site/src/locales/he/he.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: he\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Hebrew\\n\"\n\"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: he\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} מתוך {1} שורה(ות) נבחרו.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# ליבה} other {# ליבות}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} יום} two {{countString} ימים} other {{countString} ימים}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} שעה} two {{countString} שעות} other {{countString} שעות}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} דקה} two {{countString} דקות} other {{countString} דקות}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# תהליכון} other {# תהליכונים}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"שעה\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"דקה אחת\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"דקה אחת\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"שבוע אחד\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 שעות\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 דק'\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 שעות\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 ימים\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 דק'\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"פעולות\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"פעיל\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"התראות פעילות\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"מצב פעיל\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"הוסף {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"הוסף <0>מערכת</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"הוסף מערכת\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"הוסף URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"התאם אפשרויות תצוגה עבור גרפים.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"התאם את רוחב הפריסה הראשית\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"מנהל\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"אחרי\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"לאחר הגדרת משתני הסביבה, הפעל מחדש את ה-Beszel hub שלך כדי שהשינויים ייכנסו לתוקף.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"סוכן\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"היסטוריית התראות\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"התראות\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"כל הקונטיינרים\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"כל המערכות\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"האם אתה בטוח שברצונך למחוק את {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"האם אתה בטוח?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"העתקה אוטומטית דורשת הקשר מאובטח.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"ממוצע\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"ניצול ממוצע של CPU בקונטיינרים\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"הממוצע יורד מתחת ל-<0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"הממוצע עולה על <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"צריכת חשמל ממוצעת של GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"ניצול ממוצע כלל-מערכתי של CPU\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"ניצול ממוצע של {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"ניצול ממוצע של מנועי GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"גיבויים\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"רוחב פס\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"סוללה\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"סוללה\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"הפך לפעיל\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"הפך ללא פעיל\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"לפני\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"מתחת ל-{0}{1} ב-{2, plural, one {דקה האחרונה} other {-# הדקות האחרונות}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel תומך ב-OpenID Connect ובספקי אימות רבים של OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel משתמש ב-<0>Shoutrrr</0> לשילוב עם שירותי התראות פופולריים.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"בינרי\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"ביטים (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"מצב אתחול\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"בתים (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"מטמון / חוצצים\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"יכול לטעון מחדש\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"יכול להתחיל\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"יכול לעצור\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"ביטול\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"יכולות\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"קיבולת\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"זהירות - אפשרות לאובדן נתונים\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"צלזיוס (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"שנה יחידות תצוגה עבור מדדים.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"שנה אפשרויות כלליות של היישום.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"טעינה\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"בטעינה\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"אפשרויות גרף\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"בדוק את {email} לקישור איפוס.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"בדוק לוגים לפרטים נוספים\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"בדוק את שירות הניטור שלך\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"בדוק את שירות ההתראות שלך\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"נקה\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"לחץ על קונטיינר כדי לצפות במידע נוסף.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"לחץ על התקן כדי לצפות במידע נוסף.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"לחץ על מערכת כדי לצפות במידע נוסף.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"לחץ כדי להעתיק\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"הוראות שורת פקודה\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"הגדר כיצד לקבל התראות.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"אשר סיסמה\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"התנגשויות\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"החיבור נפל\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"המשך\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"הועתק ללוח\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"העתק docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"העתק docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"העתק env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"העתק מארח\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"העתק פקודת Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"העתק שם\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"העתק טקסט\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"העתק את פקודת ההתקנה עבור הסוכן למטה, או רשום סוכנים אוטומטית עם <0>token אוניברסלי</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"העתק את תוכן ה-<0>docker-compose.yml</0> עבור הסוכן למטה, או רשום סוכנים אוטומטית עם <1>token אוניברסלי</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"העתק YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"ליבות CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"שיא CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"זמן CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"פירוט זמן CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"שימוש CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"צור\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"צור חשבון\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"נוצר\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"קריטי (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"הורדה מצטברת\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"העלאה מצטברת\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"מצב נוכחי\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"מחזורים\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"יומי\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"תקופת זמן ברירת מחדל\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"מחק\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"מחק טביעת אצבע\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"תיאור\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"פרט\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"התקן\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"בפריקה\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"דיסק\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"דיסק I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"יחידת דיסק\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"שימוש בדיסק\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"שימוש בדיסק של {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"שימוש CPU של Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"שימוש זיכרון של Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"I/O של רשת Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"תיעוד\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"כבוי\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"כבוי ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"הורדה\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"משך זמן\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"ערוך\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"ערוך {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"אימייל\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"התראות אימייל\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"ריק\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"זמן סיום\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL של נקודת קצה\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL של נקודת קצה לפינג (חובה)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"הכנס כתובת אימייל לאיפוס סיסמה\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"הכנס כתובת אימייל...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"הכנס את הסיסמה החד-פעמית שלך.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"זמני\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"שגיאה\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"דוגמה:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"עלה על {0}{1} ב{2, plural, one {דקה האחרונה} other {-# הדקות האחרונות}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID ראשי של Exec\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"מערכות קיימות שלא מוגדרות ב-<0>config.yml</0> יימחקו. אנא בצע גיבויים באופן קבוע.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"יצא פעיל\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"פג תוקף לאחר שעה או בהפעלה מחדש של ה-hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"ייצא\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"ייצא תצורה\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"ייצא את תצורת המערכות הנוכחית שלך.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"פרנהייט (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"נכשל\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"מאפיינים שנכשלו:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"אימות נכשל\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"שמירת הגדרות נכשלה\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"שליחת פעימת הלב נכשלה\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"שליחת התראת בדיקה נכשלה\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"עדכון התראה נכשל\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"נכשל: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"סנן...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"טביעת אצבע\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"קושחה\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"למשך <0>{min}</0> {min, plural, one {דקה} other {דקות}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"שכחת סיסמה?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"פקודת FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"מלא\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"כללי\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"גלובלי\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"מנועי GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"צריכת חשמל GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"שימוש GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"רשת\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"בריאות\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"פעימת לב\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"ניטור פעימות לב\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"פעימת הלב נשלחה בהצלחה\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"פקודת Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"מארח / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"שיטת HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"שיטת HTTP: POST, GET, או HEAD (ברירת מחדל: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"לא פעיל\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"אם איבדת את הסיסמה לחשבון המנהל שלך, תוכל לאפס אותה באמצעות הפקודה הבאה.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"תמונה\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"לא פעיל\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"מרווח\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"כתובת אימייל לא תקינה.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"שפה\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"פריסה\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"רוחב פריסה\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"מחזור חיים\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"גבול\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"ממוצע עומס\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"ממוצע עומס 15ד\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"ממוצע עומס 1ד\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"ממוצע עומס 5ד\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"ממוצע עומס\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"מצב עומס\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"טוען...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"התנתק\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"התחבר\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"ניסיון התחברות נכשל\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"יומנים\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"מחפש איפה ליצור התראות? לחץ על סמלי הפעמון <0/> בטבלת המערכות.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID ראשי\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"נהל העדפות תצוגה והתראות.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"הוראות התקנה ידניות\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"מקס 1 דק'\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"זיכרון\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"גבול זיכרון\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"שיא זיכרון\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"שימוש בזיכרון\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"שימוש בזיכרון של קונטיינרים של Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"דגם\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"שם\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"רשת\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"תעבורת רשת של קונטיינרים של Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"תעבורת רשת של ממשקים ציבוריים\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"יחידת רשת\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"לא\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"לא נמצאו תוצאות.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"אין תוצאות.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"אין מאפייני S.M.A.R.T. זמינים עבור התקן זה.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"לא נמצאו מערכות.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"התראות\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"תמיכה ב-OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"בכל הפעלה מחדש, המערכות במסד הנתונים יעודכנו כדי להתאים למערכות המוגדרות בקובץ.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"חד-פעמי\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"סיסמה חד-פעמית\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"פתח תפריט\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"או המשך עם\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"אחר\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"דרוס התראות קיימות\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"עמוד\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"עמוד {0} מתוך {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"עמודים / הגדרות\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"סיסמה\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"הסיסמה חייבת להכיל לפחות 8 תווים.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"הסיסמה חייבת להיות פחות מ-72 בתים.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"בקשת איפוס סיסמה התקבלה\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"עבר\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"השהה\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"מושהה\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"מושהה ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"פורמט מטען (Payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"ניצול ממוצע לליבה\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"אחוז הזמן המוקדש לכל מצב\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"קבוע\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"עקביות\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"אנא <0>הגדר שרת SMTP</0> כדי להבטיח שהתראות יישלחו.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"אנא בדוק יומנים לפרטים נוספים.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"אנא בדוק את האישורים שלך ונסה שוב\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"אנא צור חשבון מנהל\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"אנא אפשר חלונות קופצים עבור אתר זה\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"אנא התחבר שוב\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"אנא ראה <0>את התיעוד</0> להוראות.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"אנא התחבר לחשבון שלך\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"פורט\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"הפעלה\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"ניצול מדויק בזמן הרשום\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"שפה מועדפת\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"תהליך התחיל\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"מפתח ציבורי\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"שעות שקט\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"קריאה\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"התקבל\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"רענן\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"קשרים\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"בקש סיסמה חד-פעמית\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"בקש OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"נדרש על ידי\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"דורש\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"איפוס סיסמה\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"נפתר\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"הפעלות מחדש\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"המשך\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"שורש\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"סובב token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"שורות לעמוד\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"מדדי זמן ריצה\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"פרטי S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"בדיקה עצמית S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"שמור כתובת באמצעות מקש enter או פסיק. השאר ריק כדי להשבית התראות אימייל.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"שמור הגדרות\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"שמור מערכת\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"נשמר במסד הנתונים ולא פג תוקף עד שתבטל אותו.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"לוח זמנים\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"קבע שעות שקט שבהן לא יישלחו התראות, כמו במהלך תקופות תחזוקה.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"קבע שעות שקט שבהן לא יישלחו התראות.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"חיפוש\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"חפש מערכות או הגדרות...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"שניות בין פינגים (ברירת מחדל: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"ראה <0>הגדרות התראות</0> כדי להגדיר כיצד אתה מקבל התראות.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"בחר {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"שלח פינג פעימת לב בודד כדי לוודא שנקודת הקצה שלך עובדת.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"שלח פינגים יוצאים תקופתיים לשירות ניטור חיצוני כדי שתוכל לנטר את Beszel מבלי לחשוף אותו לאינטרנט.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"שלח פעימת לב לבדיקה\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"נשלח\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"מספר סידורי\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"פרטי שירות\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"שירותים\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"הגדר סף אחוזים עבור צבעי מד.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"הגדר את משתני הסביבה הבאים ב-Beszel hub שלך כדי לאפשר ניטור פעימות לב:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"הגדרות\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"הגדרות נשמרו\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"התחבר\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"הגדרות SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"מיין לפי\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"זמן התחלה\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"מצב\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"סטטוס\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"מצב משני\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"שטח swap בשימוש על ידי המערכת\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"שימוש ב-Swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"מערכת\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"ממוצעי עומס מערכת לאורך זמן\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"שירותי Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"מערכות\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"מערכות יכולות להיות מנוהלות בקובץ <0>config.yml</0> בתוך ספריית הנתונים שלך.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"טבלה\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"משימות\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"טמפ'\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"טמפרטורה\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"יחידת טמפרטורה\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"טמפרטורות של חיישני המערכת\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"בדוק <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"בדוק פעימת לב\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"התראת בדיקה נשלחה\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"הסטטוס הכללי הוא <0>ok</0> כשכל המערכות פועלות, <1>warn</1> כשמופעלות התראות, ו-<2>error</2> כשמערכת כלשהי מושבתת.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"לאחר מכן התחבר ל-backend ואפס את סיסמת חשבון המשתמש שלך בטבלת המשתמשים.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"פעולה זו לא ניתנת לביטול. פעולה זו תמחק לצמיתות את כל הרשומות הנוכחיות עבור {name} ממסד הנתונים.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"פעולה זו תמחק לצמיתות את כל הרשומות שנבחרו ממסד הנתונים.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"תפוקה של {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"תפוקה של מערכת הקבצים הראשית\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"פורמט זמן\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"לאימייל(ים)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"החלף רשת\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"החלף ערכת נושא\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens וטביעות אצבע\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens מאפשרים לסוכנים להתחבר ולהירשם. טביעות אצבע הן מזהים יציבים ייחודיים לכל מערכת, מוגדרים בחיבור הראשון.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens וטביעות אצבע משמשים לאימות חיבורי WebSocket ל-hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"כולל\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"סך נתונים שהתקבלו עבור כל ממשק\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"סך נתונים שנשלחו עבור כל ממשק\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"סה\\\"כ: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"הופעל על ידי\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"מפעילים\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"מופעל כאשר ממוצע העומס לדקה אחת עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"מופעל כאשר ממוצע העומס ל-15 דקות עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"מופעל כאשר ממוצע העומס ל-5 דקות עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"מופעל כאשר כל חיישן עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"מופעל כאשר טעינת הסוללה יורדת מתחת לסף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"מופעל כאשר השילוב של למעלה/למטה עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"מופעל כאשר שימוש CPU עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"מופעל כאשר שימוש GPU עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"מופעל כאשר שימוש בזיכרון עולה על סף\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"מופעל כאשר הסטטוס מתחלף בין למעלה ולמטה\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"מופעל כאשר שימוש בכל דיסק עולה על סף\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"סוג\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"קובץ יחידה\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"העדפות יחידות\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"token אוניברסלי\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"לא ידוע\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"ללא הגבלה\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"למעלה\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"למעלה ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"עדכן\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"עודכן\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"מתעדכן כל 10 דקות.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"העלאה\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"זמן פעילות\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"שימוש\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"שימוש במחיצה הראשית\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"בשימוש\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"משתמשים\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"ערך\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"צפה\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"צפה בעוד\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"צפה ב-200 ההתראות האחרונות שלך.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"שדות גלויים\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"ממתין לרשומות מספיקות לתצוגה\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"רוצה לעזור לשפר את התרגומים שלנו? בדוק <0>Crowdin</0> לפרטים.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"רוצה\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"אזהרה (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"ספי אזהרה\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / התראות דחיפה\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"כאשר מופעל, אסימון זה מאפשר לסוכנים להירשם באופן עצמי ללא יצירת מערכת מוקדמת.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"בשימוש ב-POST, כל פעימת לב כוללת מטען JSON עם סיכום סטטוס המערכת, רשימת מערכות מושבתות והתראות שהופעלו.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"פקודת Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"כתיבה\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"תצורת YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"תצורת YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"כן\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"הגדרות המשתמש שלך עודכנו.\"\n"
  },
  {
    "path": "internal/site/src/locales/hr/hr.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hr\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Croatian\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: hr\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} od {1} redaka izabrano.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# jezgra} few {# jezgre} other {# jezgri}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dan} other {{countString} dani}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} sat} other {{countString} sati}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuta} few {{countString} minuta} many {{countString} minuta} other {{countString} minute}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# nit} few {# niti} other {# niti}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 sat\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 minut\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuta\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 tjedan\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 sati\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 minuta\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 sati\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dana\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 minuta\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Akcije\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktivan\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktivna Upozorenja\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktivno stanje\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Dodaj {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Dodaj <0>Sustav</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Dodaj sustav\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Dodaj URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Podesite opcije prikaza grafikona.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Prilagodite širinu glavnog rasporeda\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Nakon\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Nakon postavljanja varijabli okruženja, ponovno pokrenite svoj Beszel hub kako bi promjene stupile na snagu.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Povijest Upozorenja\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Upozorenja\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Svi spremnici\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Svi Sustavi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Jeste li sigurni da želite izbrisati {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Jeste li sigurni?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatsko kopiranje zahtijeva siguran kontekst.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Prosjek\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Prosječna iskorištenost procesora u spremnicima\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Prosjek pada ispod <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Prosjek premašuje <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Prosječna potrošnja energije grafičkog procesora\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Prosječna iskorištenost procesora u cijelom sustavu\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Prosječna iskorištenost {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Prosječna iskorištenost grafičkih procesora\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Sigurnosne kopije\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Mrežna Propusnost\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Baterija\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Postalo aktivno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Postalo neaktivno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Prije\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Ispod {0}{1} u posljednjih {2, plural, one {# minuti} few {# minute} other {# minuta}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel podržava OpenID Connect i mnoge druge pružatelje OAuth2  autentifikacije.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel koristi <0>Shoutrrr</0> za integraciju s popularnim obavještajnim uslugama.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binarni\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bitovi (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Stanje pokretanja\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bajtovi (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Predmemorija / Međuspremnici\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Može se ponovno učitati\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Može se pokrenuti\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Može se zaustaviti\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Otkaži\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Mogućnosti\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapacitet\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Oprez - mogući gubitak podataka\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Promijenite mjerene jedinice korištene za prikazivanje podataka.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Promijenite opće opcije aplikacije.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Punjenje\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Puni se\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Postavke grafikona\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Provjerite {email} za pristup poveznici za resetiranje.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Provjerite zapise (logove) za više detalja.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Provjerite svoju uslugu nadzora\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Provjerite svoju obavještajnu uslugu\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Očisti\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Kliknite na spremnik za prikaz više informacija.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Kliknite na uređaj da biste vidjeli više informacija.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Odaberite sustav za prikaz više informacija.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Pritisnite za kopiranje\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Upute za naredbeni redak\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurirajte način primanja obavijesti upozorenja.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Potvrdi lozinku\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Sukobi\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Veza je pala\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Nastavi\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Kopirano u međuspremnik\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopiraj docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Kopiraj docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopiraj env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopiraj hosta\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopiraj Linux komandu\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopiraj naziv\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopiraj tekst\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopirajte instalacijsku komandu za opisanog agenta ili automatski registrirajte agenta uz pomoć <0>sveopćeg tokena</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopirajte sadržaj <0>docker-compose.yml</0> datoteke za opisanog agenta ili automatski registrirajte agenta uz pomoć <1>sveopćeg tokena</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopiraj YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"Procesor\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU jezgre\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU vrhunac\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU vrijeme\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Raspodjela CPU vremena\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Iskorištenost procesora\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Stvori\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Napravite račun\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Kreiran\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritično (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativno preuzimanje\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativno otpremanje\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Trenutno stanje\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Ciklusi\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Dnevno\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Zadano vremensko razdoblje\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Izbriši\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Izbriši otisak\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Opis\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detalj\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Uređaj\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Prazni se\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Mjerna jedinica za disk\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Iskorištenost Diska\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Iskorištenost diska od {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Iskorištenost Docker procesora\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Iskorištenost Docker memorije\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker mrežni I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentacija\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Sustav je pao\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Sustav je pao ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Preuzmi\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Trajanje\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Uredi\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Uredi {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Email obavijesti\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Prazno\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Vrijeme završetka\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL krajnje točke\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL krajnje točke za pinganje (obavezno)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Unesite email adresu kako biste resetirali lozinku\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Unesite email adresu...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Unesite jednokratnu lozinku.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Efemeran\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Greška\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Primjer:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Premašuje {0}{1} u posljednjih {2, plural, one {# minuta} other {# minute}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Glavni PID izvršavanja\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Postojeći sustavi koji nisu definirani u <0>config.yml</0> datoteci bit će izbrisani. Molimo Vas da spremate redovite sigurnosne kopije.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Izašlo aktivno\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Istječe nakon jednog sata ili ponovnog pokretanja huba.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Izvoz\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Izvoz konfiguracije\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Izvoz trenutne sistemske konfiguracije.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Farenhajt (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Neuspješno\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Neuspjeli atributi:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Neuspješna provjera autentičnosti\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Neuspješno spremanje postavki\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Slanje heartbeata nije uspjelo\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Neuspješno slanje probne obavijesti\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Neuspješno ažuriranje upozorenja\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Neuspjelo: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtriraj...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Otisak\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmver\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Za <0>{min}</0> {min, plural, one {minutu} other {minute}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Zaboravljena lozinka?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD naredba\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Puno\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Općenito\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globalno\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Grafički procesori\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Energetska potrošnja grafičkog procesora\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Iskorištenost GPU-a\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Rešetka\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Zdravlje\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Nadzor heartbeata\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat uspješno poslan\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew naredba\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP metoda\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP metoda: POST, GET ili HEAD (zadano: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Neaktivno\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Ako ste izgubili lozinku za svoj administratorski račun, možete ju resetirati pomoću sljedeće naredbe.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Slika\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Neaktivno\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Nevažeća email adresa.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Jezik\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Izgled\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Širina rasporeda\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Životni ciklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"ograničenje\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Prosječno Opterećenje\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Prosječno Opterećenje 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Prosječno Opterećenje 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Prosječno Opterećenje 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Prosječno Opterećenje\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Stanje učitavanja\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Učitavanje...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Odjava\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Prijava\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Neuspješno pokušaj prijave\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Zapisi\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Tražite gdje stvoriti upozorenja? Kliknite ikonu zvona <0/> u tablici sustava.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Glavni PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Upravljajte postavkama prikaza i obavijesti.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Upute za ručno postavljanje\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maksimalno 1 minuta\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memorija\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Ograničenje memorije\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Vrhunac memorije\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Iskorištenost memorije\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Iskorištenost memorije Docker spremnika\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Ime\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Mreža\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Mrežni promet Docker spremnika\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Mrežni promet javnih sučelja\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Mjerna jedinica za mrežu\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Ne\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nema rezultata.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Nema rezultata.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Nema dostupnih S.M.A.R.T. atributa za ovaj uređaj.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nije pronađen nijedan sustav.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Obavijesti\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Podrška za OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Prilikom svakog ponovnog pokretanja, sustavi u bazi podataka bit će ažurirani kako bi odgovarali sustavima definiranim u datoteci.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Jednokratno\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Jednokratna lozinka\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Otvori meni\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Ili nastavi s\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Ostalo\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Prebriši postojeća upozorenja\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Stranica\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Stranica {0} od {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Stranice / Postavke\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Lozinka\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Lozinka mora imati najmanje 8 znakova.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Lozinka mora biti kraća od 72 bajta.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Zahtjev za ponovno postavljanje lozinke zaprimljen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Prošlost\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pauza\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pauzirano\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pauzirano ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Format korisnog tereta (Payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Prosječna iskorištenost po jezgri\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Postotak vremena provedenog u svakom stanju\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Trajan\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Postojanost\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Molimo <0>konfigurirajte SMTP server</0> kako biste osigurali isporuku upozorenja.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Za više detalja provjerite zapise (logove).\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Provjerite svoje vjerodajnice i pokušajte ponovno\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Molimo kreirajte administrativan račun\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Molimo omogućite skočne prozore za ovu stranicu\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Molimo prijavite se ponovno\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Molimo provjerite <0>dokumentaciju</0> za upute.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Molimo prijavite se u svoj račun\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Uključivanje\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Precizno iskorištenje u zabilježenom vremenu\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Preferirani jezik\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces pokrenut\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Javni Ključ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Tihi sati\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Pročitaj\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Primljeno\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Osvježi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Odnosi\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Zatraži jednokratnu lozinku\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Zatraži OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Zahtijeva\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Zahtijeva\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Resetiraj Lozinku\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Razrješeno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Ponovna pokretanja\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Nastavi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Korijen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Promijeni token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Redovi po stranici\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metrike izvršavanja\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Detalji\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Samotestiranje\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Spremite adresu pomoću tipke enter ili zareza. Ostavite prazno kako biste onemogućili obavijesti e-poštom.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Spremi Postavke\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Spremi sustav\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Spremljeno u bazi podataka i ne istječe dok ga ne onemogućite.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Raspored\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Rasporedi tihe sate kada se obavijesti neće slati, na primjer tijekom razdoblja održavanja.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Rasporedi tihe sate kada se obavijesti neće slati.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Pretraži\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Pretraži za sisteme ili postavke...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunde između pingova (zadano: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Pogledajte <0>postavke obavijesti</0> da biste konfigurirali način primanja upozorenja.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Odaberi {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Pošaljite jedan heartbeat ping kako biste provjerili radi li vaša krajnja točka.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Šaljite povremene odlazne pingove vanjskoj usluzi nadzora kako biste mogli nadzirati Beszel bez izlaganja internetu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Pošalji testni heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Poslano\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serijski broj\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Detalji usluge\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Usluge\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Postavite pragove postotka za boje mjerača.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Postavite sljedeće varijable okruženja na svom Beszel hubu kako biste omogućili nadzor heartbeata:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Postavke\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Postavke spremljene\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Prijava\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP postavke\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sortiraj po\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Vrijeme početka\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Stanje\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Podstanje\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap prostor uzet od strane sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap Iskorištenost\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistem\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Prosječno opterećenje sustava kroz vrijeme\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd servisi\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sustavi\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Sistemima se može upravljati u <0>config.yml</0> datoteci unutar data direktorija.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tablica\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Zadaci\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Mjerna jedinica za temperaturu\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperature sistemskih senzora\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Testni <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testiraj heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testna obavijest poslana\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Ukupni status je <0>ok</0> kada su svi sustavi u radu, <1>warn</1> kada su aktivirana upozorenja i <2>error</2> kada je bilo koji sustav isključen.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Zatim se prijavite u backend i resetirajte lozinku korisničkog računa u tablici korisnika.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve trenutne zapise za {name} iz baze podataka.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Ovom radnjom će se trajno izbrisati svi odabrani zapisi iz baze podataka.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Protok {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Protok root datotečnog sustava\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Format vremena\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Primaoci e-pošte\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Uključi/isključi rešetku\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Uključi/isključi temu\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokeni & Otisci\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokeni dopuštaju agentima prijavu i registraciju. Otisci su stabilni identifikatori jedinstveni svakom sustavu, koji se postavljaju prilikom prvog spajanja.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokeni se uz otiske koriste za autentifikaciju WebSocket veza prema središnjoj kontroli.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Ukupno\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Ukupni podaci primljeni za svako sučelje\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Ukupni podaci poslani za svako sučelje\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Ukupno: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Pokrenuto od\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Okidači\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Pokreće se kada prosječna opterećenost sustava unutar 1 minute prijeđe prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Pokreće se kada prosječna opterećenost sustava unutar 15 minuta prijeđe prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Pokreće se kada prosječna opterećenost sustava unutar 5 minuta prijeđe prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Pokreće se kada bilo koji senzor prijeđe prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Pokreće se kada razina baterije padne ispod praga\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Pokreće se kada kombinacija gore/dolje premaši prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Pokreće se kada iskorištenost procesora premaši prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Pokreće se kada iskorištenost GPU-a premaši prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Pokreće se kada iskorištenost memorije premaši prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Pokreće se kada se status sistema promijeni\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Pokreće se kada iskorištenost bilo kojeg diska premaši prag\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Vrsta\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Datoteka jedinice\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Opcije mjernih jedinica\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Sveopći token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Nepoznata\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Neograničeno\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Sustav je podignut\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Sustav je podignut ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Ažuriraj\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Ažurirano\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Ažurirano svakih 10 minuta.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Otpremi\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Iskorištenost\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Iskorištenost root datotečnog sustava\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Iskorišteno\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Korisnici\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Vrijednost\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Prikaz\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Prikaži više\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Pogledajte posljednjih 200 upozorenja.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Vidljiva polja\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Čeka se na više podataka prije prikaza\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Želite li nam pomoći da naše prijevode učinimo još boljim? Posjetite <0>Crowdin</0> za više detalja.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Želi\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Upozorenje (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Pragovi upozorenja\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push obavijest\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Kada je omogućen, ovaj token omogućuje agentima da se sami registriraju bez prethodnog stvaranja sustava.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Kada koristite POST, svaki heartbeat uključuje JSON payload sa sažetkom statusa sustava, popisom isključenih sustava i aktiviranim upozorenjima.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows naredba\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Piši\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML konfiguracija\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Konfiguracija\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Da\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Vaše korisničke postavke su ažurirane.\"\n"
  },
  {
    "path": "internal/site/src/locales/hu/hu.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: hu\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Hungarian\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: hu\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} a(z) {1} sorból kiválasztva.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# mag} other {# mag}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} nap} other {{countString} nap}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} óra} other {{countString} óra}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} perc} few {{countString} perc} many {{countString} perc} other {{countString} perc}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# szál} other {# szál}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 óra\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 perc\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 perc\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 hét\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 óra\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 perc\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 óra\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 nap\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 perc\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Műveletek\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktív\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktív riasztások\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktív állapot\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Hozzáadás {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"<0>Rendszer</0> Hozzáadása\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Rendszer hozzáadása\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"URL hozzáadása\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"A diagramok megjelenítésének beállítása.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"A fő elrendezés szélességének beállítása\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Adminisztráció\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Utána\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"A környezeti változók beállítása után indítsa újra a Beszel hubot a módosítások érvénybe léptetéséhez.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Ügynök\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Riasztási előzmények\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Riasztások\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Minden konténer\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Minden rendszer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Biztosan törölni szeretnéd {name}-t?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Biztos vagy benne?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Az automatikus másolás biztonságos környezetet igényel.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Átlag\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Konténerek átlagos CPU kihasználtsága\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Az átlag esik <0>{value}{0}</0> alá\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Az átlag meghaladja a <0>{value}{0}</0> értéket\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU-k átlagos energiafogyasztása\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Rendszerszintű CPU átlagos kihasználtság\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0} átlagos kihasználtsága\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU-k átlagos kihasználtsága\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Biztonsági mentések\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Sávszélesség\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Akku\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Akkumulátor\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Aktívvá vált\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Inaktívvá vált\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Előtte\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"{0}{1} alatt az elmúlt {2, plural, one {# percben} other {# percben}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"A Beszel támogatja az OpenID Connect-et és számos OAuth2 hitelesítési szolgáltatót.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"A Beszel a <0>Shoutrrr</0>-t használja a népszerű értesítési szolgáltatások integrálására.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Bináris\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bitek (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Indítási állapot\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Byte-ok (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Gyorsítótár / Pufferelések\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Újratölthető\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Indítható\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Leállítható\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Mégsem\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Képességek\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapacitás\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Figyelem - potenciális adatvesztés\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"A mértékegységek megjelenítésének megváltoztatása.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Általános alkalmazásbeállítások módosítása.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Töltés\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Töltődik\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Diagram beállítások\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Ellenőrizd a {email} címet a visszaállító linkért.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Ellenőrizd a naplót a további részletekért.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Ellenőrizze a megfigyelő szolgáltatást\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Ellenőrizd az értesítési szolgáltatásodat\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Törlés\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Kattintson egy konténerre a további információk megtekintéséhez.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Kattintson egy eszközre további információk megtekintéséhez.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"További információkért kattints egy rendszerre.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Kattints a másoláshoz\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Parancssori utasítások\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfiguráld, hogyan kapod az értesítéseket.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Jelszó megerősítése\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konfliktusok\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Kapcsolat megszakadt\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Tovább\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Vágólapra másolva\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Docker compose másolása\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Docker run másolása\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Környezet másolása\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Hoszt másolása\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Linux parancs másolása\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Név másolása\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Szöveg másolása\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Másold az alábbi ügynök telepítési parancsát, vagy regisztráld az ügynököket automatikusan egy <0>univerzális tokennel</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Másold az alábbi ügynök <0>docker-compose.yml</0> tartalmát, vagy regisztráld az ügynököket automatikusan egy <1>univerzális tokennel</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAML másolása\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU magok\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU csúcs\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU idő\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU idő felbontása\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU használat\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Létrehozás\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Fiók létrehozása\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Létrehozva\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritikus (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulatív letöltés\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulatív feltöltés\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Jelenlegi állapot\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Ciklusok\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Napi\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Alapértelmezett időszak\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Törlés\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Ujjlenyomat törlése\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Leírás\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Részlet\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Eszköz\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Kisül\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Lemez\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Lemez I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Lemez mértékegysége\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Lemezhasználat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Lemezhasználat a {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU használat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker memória használat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker hálózat I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentáció\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Offline\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Offline ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Letöltés\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Időtartam\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Szerkesztés\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Szerkesztés {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-mail értesítések\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Üres\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Befejezés ideje\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Végpont URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Pingelendő végpont URL (kötelező)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"E-mail cím megadása a jelszó visszaállításához\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Adja meg az e-mail címet...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Adja meg az egyszeri jelszavát.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Ideiglenes\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Hiba\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Példa:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Túllépi a {0}{1} értéket az elmúlt {2, plural, one {# percben} other {# percben}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Fő folyamat PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"A <0>config.yml</0> fájlban nem definiált meglévő rendszerek törlésre kerülnek. Kérjük, készítsen rendszeres biztonsági mentéseket.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Aktívként kilépett\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Lejár egy óra után vagy a hub újraindításakor.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportálás\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Konfiguráció exportálása\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exportálja a jelenlegi rendszerkonfigurációt.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Sikertelen\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Sikertelen attribútumok:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Hitelesítés sikertelen\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Nem sikerült menteni a beállításokat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Nem sikerült elküldeni a szívverést (heartbeat)\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Teszt értesítés elküldése sikertelen\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Nem sikerült frissíteni a riasztást\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Sikertelen: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Szűrő...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Ujjlenyomat\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"<0>{min}</0> {min, plural, one {percig} other {percig}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Elfelejtette a jelszavát?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD parancs\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Tele\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Általános\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globális\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-k\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU áramfelvétele\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU használat\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Rács\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Egészség\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat figyelés\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat sikeresen elküldve\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew parancs\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Állomás / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP metódus\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP metódus: POST, GET vagy HEAD (alapértelmezett: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Tétlen\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Ha elvesztette az admin fiók jelszavát, a következő paranccsal állíthatja vissza.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Kép\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inaktív\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervallum\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Érvénytelen e-mail cím.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Nyelv\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Elrendezés\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Elrendezés szélessége\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Életciklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"korlát\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Terhelési átlag\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Terhelési átlag 15p\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Terhelési átlag 1p\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Terhelési átlag 5p\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Terhelési átlag\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Betöltési állapot\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Betöltés...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Kijelentkezés\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Bejelentkezés\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Bejelentkezés sikertelen\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Naplók\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Inkább azt keresi, hogy hol hozhat létre riasztásokat? Kattintson a csengő <0/> ikonokra a rendszerek táblázatában.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Fő PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"A megjelenítési és értesítési beállítások kezelése.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Manuális beállítási lépések\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maximum 1 perc\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"RAM\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Memória korlát\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Memória csúcs\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Memóriahasználat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker konténerek memória használata\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modell\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Név\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Hálózat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker konténerek hálózati forgalma\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Nyilvános interfészek hálózati forgalma\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Sávszélesség mértékegysége\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nem\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nincs találat.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Nincs találat.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Ehhez az eszközhöz nem állnak rendelkezésre S.M.A.R.T. attribútumok.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nem található rendszer.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Értesítések\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC támogatás\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Minden újraindításkor az adatbázisban lévő rendszerek frissítésre kerülnek, hogy megfeleljenek a fájlban meghatározott rendszereknek.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Egyszeri\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Egyszeri jelszó\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Menü megnyitása\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Vagy folytasd ezzel\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Egyéb\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Felülírja a meglévő riasztásokat\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Oldal\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"{0}/{1} oldal\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Oldalak / Beállítások\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Jelszó\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"A jelszónak legalább 8 karakternek kell lennie.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"A jelszó legfeljebb 72 byte lehet.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Jelszó-visszaállítási kérelmet kaptunk\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Múlt\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Szüneteltetés\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Szüneteltetve\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Szüneteltetve ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Payload formátum\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Átlagos kihasználtság magonként\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Az idő százalékos aránya minden állapotban\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Állandó\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Tartósság\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Kérjük, <0>konfigurálj egy SMTP szervert</0> az értesítések kézbesítésének biztosítása érdekében.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Kérjük, ellenőrizd a naplókat a további részletekért.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Kérjük, ellenőrizze a hitelesítő adatait, és próbálja újra\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Kérjük, hozzon létre egy admin fiókot\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Kérjük, engedélyezze a felugró ablakokat ezen az oldalon\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Kérjük jelentkezz be újra\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Kérjük, nézze meg <0>a dokumentációt</0> az utasításokért.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Kérjük, jelentkezzen be a fiókjába\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Bekapcsolás\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Pontos kihasználás a rögzített időpontban\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Preferált nyelv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Folyamat elindítva\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Nyilvános kulcs\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Csendes órák\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Olvasás\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Fogadott\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Frissítés\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Kapcsolatok\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Egyszeri jelszó kérése\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP kérése\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Szükséges\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Igényel\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Jelszó visszaállítása\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Megoldva\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Újraindítások\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Folytatás\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Gyökér\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Tokenváltás\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Sorok száma oldalanként\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Futásidejű metrikák\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Részletek\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Önteszt\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Mentse el a címet az Enter billentyű vagy a vessző használatával. Hagyja üresen az e-mail értesítések letiltásához.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Beállítások mentése\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Rendszer mentése\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Elmentve az adatbázisban és nem jár le, amíg ki nem kapcsolod.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Ütemezés\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Ütemezze a csendes órákat, amikor az értesítések nem kerülnek elküldésre, például karbantartási időszakokban.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Ütemezze a csendes órákat, amikor az értesítések nem kerülnek elküldésre.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Keresés\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Keresés rendszerek vagy beállítások után...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Pingek közötti másodpercek (alapértelmezett: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Lásd <0>az értesítési beállításokat</0>, hogy konfigurálja, hogyan kap értesítéseket.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"{foo} kiválasztása\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Küldjön egyetlen heartbeat pinget a végpont működésének ellenőrzéséhez.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Küldjön időszakos kimenő pingeket egy külső megfigyelő szolgáltatásnak, így a Beszel-t az internetnek való kitettség nélkül is megfigyelheti.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Teszt heartbeat küldése\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Elküldve\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Sorozatszám\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Szolgáltatás részletei\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Szolgáltatások\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Százalékos küszöbértékek beállítása a mérőszínekhez.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Állítsa be a következő környezeti változókat a Beszel hubon a heartbeat figyelés engedélyezéséhez:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Beállítások\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Beállítások elmentve\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Bejelentkezés\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP beállítások\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Rendezés\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Kezdési idő\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Állapot\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Állapot\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Részállapot\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Rendszer által használt swap terület\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap használat\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Rendszer\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Rendszer terhelési átlaga\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd szolgáltatások\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Rendszer\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"A rendszereket egy <0>config.yml</0> fájlban lehet kezelni az adatkönyvtárban.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tábla\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Feladatok\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Hőmérséklet\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Hőmérséklet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Hőmérséklet mértékegysége\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"A rendszer érzékelőinek hőmérséklete\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Teszt <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Teszt heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Teszt értesítés elküldve\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Az összesített állapot <0>ok</0>, ha minden rendszer fut, <1>warn</1>, ha riasztások léptek fel, és <2>error</2>, ha bármelyik rendszer leállt.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Ezután jelentkezzen be a backendbe, és állítsa vissza a felhasználói fiók jelszavát a felhasználók táblázatban.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Ezt a műveletet nem lehet visszavonni! Véglegesen törli a {name} összes jelenlegi rekordját az adatbázisból!\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Ez véglegesen törli az összes kijelölt bejegyzést az adatbázisból.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"A {extraFsName} átviteli teljesítménye\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"A gyökér fájlrendszer átviteli teljesítménye\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Időformátum\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"E-mailben\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Rács ki- és bekapcsolása\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Téma váltása\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokenek & Ujjlenyomatok\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"A tokenek lehetővé teszik az ügynökök csatlakozását és regisztrációját. A fingeravtrykkok stabil azonosítók, amelyek minden rendszerre egyediek, és az első kapcsolódáskor kerülnek beállításra.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"A tokeneket és fingeravtrykkokat a hubhoz való WebSocket kapcsolatok hitelesítésére használják.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Összesen\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Összes fogadott adat minden interfészenként\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Összes elküldött adat minden interfészenként\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Összesen: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Kiváltva\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Kiváltók\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Riaszt, ha az 1 perces terhelési átlag túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Riaszt, ha a 15 perces terhelési átlag túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Riaszt, ha bármelyik hőmérséklet érzékelő túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Riaszt, ha az akkumulátor töltöttségi szintje egy küszöbérték alá esik\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Riaszt, ha a sávszélesség-használat túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Riaszt, ha a CPU használat túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Riaszt, ha a GPU használat túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Riaszt, ha a memóriahasználat túllép egy küszöbértéket\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Riaszt, amikor a rendszer online állapota változik\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Riaszt, ha a lemezhasználat túllép egy küszöbértéket\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Típus\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Egység fájl\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Mértékegység beállítások\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Univerzális token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Ismeretlen\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Korlátlan\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Online\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Online ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Frissítés\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Frissítve\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"10 percenként frissítve.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Feltöltés\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Üzemidő\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Használat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Root partíció kihasználtsága\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Felhasznált\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Felhasználók\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Érték\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Nézet\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Továbbiak megjelenítése\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Legfrissebb 200 riasztásod áttekintése.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Látható mezők\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Elegendő rekordra várva a megjelenítéshez\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Szeretne segíteni nekünk abban, hogy fordításaink még jobbak legyenek? További részletekért nézze meg a <0>Crowdin</0> honlapot.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Igényel\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Figyelmeztetés (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Figyelmeztetési küszöbértékek\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push értesítések\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Ha engedélyezve van, ez a token lehetővé teszi az ügynökök számára a regisztrációt a rendszer előzetes létrehozása nélkül.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"POST használata esetén minden heartbeat tartalmaz egy JSON payload-ot a rendszerállapot összefoglalójával, a leállt rendszerek listájával és a kiváltott riasztásokkal.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows parancs\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Írás\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML konfiguráció\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML konfiguráció\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Igen\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"A felhasználói beállítások frissítésre kerültek.\"\n"
  },
  {
    "path": "internal/site/src/locales/id/id.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: id\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Indonesian\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: id\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} dari {1} baris terpilih.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# inti} other {# inti}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} hari} other {{countString} hari}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} jam} other {{countString} jam}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} menit} few {{countString} menit} many {{countString} menit} other {{countString} menit}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# thread} other {# thread}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 jam\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 mnt\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 menit\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 minggu\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 jam\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 menit\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 jam\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 hari\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 mnt\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Aksi\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktif\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Peringatan Aktif\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Status aktif\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Tambah {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Tambah <0>Sistem</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Tambah sistem\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Tambah URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Sesuaikan tampilan grafik.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Sesuaikan lebar layar utama\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Setelah\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Setelah mengatur variabel lingkungan, restart hub Beszel Anda agar perubahan dapat diterapkan.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agen\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Riwayat Peringatan\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Peringatan\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Semua Container\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Semua Sistem\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Apakah anda yakin ingin menghapus {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Apakah anda yakin?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Copy memerlukan https.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Rata-rata\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Rata-rata utilisasi CPU untuk semua kontainer\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Rata-rata turun di bawah <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Rata-rata melebihi <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Rata-rata konsumsi daya GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Rata-rata utilisasi CPU seluruh sistem\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Rata-rata utilisasi {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Rata-rata utilisasi GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Cadangan\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandwith\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Baterai\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Baterai\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Menjadi aktif\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Menjadi inaktif\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Sebelum\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Di bawah {0}{1} dalam {2, plural, one {# menit} other {# menit}} terakhir\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel mendukung OpenID Connect dan OAuth2 dari berbagai penyedia layanan.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel menggunakan <0>Shoutrrr</0> untuk mengintegrasikan dengan penyedia layanan notifikasi.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binari\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bit (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Status memulai\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Byte (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffers\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Dapat dimuatulang\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Dapat dimulai\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Dapat diberhentikan\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Batal\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Kapabilitas\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapasitas\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Perhatian - potensi kehilangan data\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Ubah tampilan satuan untuk metrik.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Ubah pengaturan umum aplikasi.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Isi baterai\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Sedang mengisi\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Pilihan grafik\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Periksa {email} untuk tautan atur ulang password.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Periksa riwayat untuk lebih detail.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Periksa layanan pemantauan Anda\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Periksa jasa penyedia notifikasi anda\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Bersihkan\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klik pada kontainer untuk melihat informasi lebih banyak.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klik pada perangkat untuk melihat informasi lebih banyak.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klik pada sistem untuk melihat informasi lebih banyak.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klik untuk menyalin\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instruksi command line\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurasi bagaimana anda menerima notifikasi peringatan.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Konfirmasi kata sandi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflik\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Koneksi terputus\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Lanjutkan\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Disalin ke clipboard\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Salin docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Salin docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Salin env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Salin host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Salin perintah Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Salin nama\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Salin teks\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Salin perintah instalasi untuk agen di bawah, atau daftarkan agen secara otomatis dengan <0>universal token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Salin konten <0>docker-compose.yml</0> untuk agen di bawah, atau daftarkan agen secara otomatis dengan <1>universal token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Salin YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Inti CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Puncak CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Waktu CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Rincian Waktu CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Penggunaan CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Buat\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Buat akun\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Dibuat\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritis (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Akumulasi Download\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Akumulasi Upload\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Status saat ini\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Siklus\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Harian\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standar waktu\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Hapus\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Hapus fingerprint\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Deskripsi\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detail\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Perangkat\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Sedang tidak di charge\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Unit disk\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Penggunaan Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Penggunaan disk dari {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Penggunaan CPU Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Penggunaan Memori Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Network I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentasi\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Mati\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Mati ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Unduh\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Durasi\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Ubah\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Ubah {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Notifikasi email\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Kosong\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Waktu Berakhir\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL Titik Akhir\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL titik akhir untuk di-ping (diperlukan)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Masukkan alamat email untuk mereset kata sandi\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Masukkan alamat email...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Masukkan otp anda.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Sementara\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Error\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Contoh:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Melebihi {0}{1} dalam {2, plural, one {# menit} other {# menit}} terakhir\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID utama exec\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Sistem yang ada yang tidak didefinisikan dalam <0>config.yml</0> akan dihapus. Silakan buat cadangan secara teratur.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Keluar aktif\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Kedaluwarsa setelah satu jam atau saat restart hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Ekspor\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Export konfigurasi\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Export konfigurasi sistem anda saat ini.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Gagal\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Atribut yang Gagal:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Gagal mengautentikasi\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Gagal menyimpan pengaturan\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Gagal mengirim heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Gagal mengirim tes notifikasi\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Gagal memperbarui peringatan\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Gagal: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Saring...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Sidik jari\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Untuk <0>{min}</0> {min, plural, one {menit} other {menit}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Lupa kata sandi?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Perintah FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Penuh\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Umum\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Konsumsi Daya GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Penggunaan GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Kartu\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Kesehatan\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Pemantauan Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat berhasil dikirim\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Perintah Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Metode HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Metode HTTP: POST, GET, atau HEAD (default: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Idle\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Jika anda kehilangan kata sandi untuk akun admin anda, anda dapat meresetnya menggunakan perintah berikut.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Image\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Tidak aktif\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Alamat email tidak valid.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Bahasa\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Tampilan\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Lebar tampilan\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Siklus hidup\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"batas\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Rata-rata Beban\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Rata-rata Beban 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Rata-rata Beban 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Rata-rata Beban 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Rata-rata Beban\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Beban saat ini\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Memuat...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Keluar\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Masuk\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Percobaan masuk gagal\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Log\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Mencari tempat untuk membuat peringatan? Klik ikon lonceng <0/> di tabel sistem.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID utama\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Kelola preferensi tampilan dan notifikasi.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Instruksi setup manual\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maks 1 mnt\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memori\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Batas memori\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Puncak Memori\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Penggunaan Memori\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Penggunaan memori kontainer docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nama\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Jaringan\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Trafik jaringan kontainer docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Trafik jaringan antarmuka publik\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Unit jaringan\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Tidak\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Tidak ada hasil ditemukan.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Tidak ada hasil.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Tidak ada atribut S.M.A.R.T. yang tersedia untuk perangkat ini.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Sistem tidak ditemukan.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notifikasi\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Dukungan OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Pada setiap restart, sistem dalam database akan diperbarui untuk mencocokkan sistem yang didefinisikan dalam file.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Sekali pakai\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Kata sandi sekali pakai (OTP)\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Buka menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Atau lanjutkan dengan\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Lainnya\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Timpa peringatan yang ada\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Halaman\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Halaman {0} dari {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Halaman / Pengaturan\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Kata sandi\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Kata sandi harus minimal 8 karakter.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Kata sandi harus kurang dari 72 byte.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Permintaan reset kata sandi diterima\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Lalu\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Jeda\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Dijeda\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Dijeda ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Format payload\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Rata-rata utilisasi per-inti\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Persentase waktu yang dihabiskan di setiap status\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Tetap berlaku\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Silakan <0>konfigurasi server SMTP</0> untuk memastikan peringatan dikirimkan.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Silakan periksa log untuk detail lebih lanjut.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Silakan periksa kredensial anda dan coba lagi\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Silakan buat akun admin\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Silakan aktifkan pop-up untuk situs ini\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Silakan masuk lagi\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Silakan lihat <0>dokumentasi</0> untuk instruksi.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Silakan masuk ke akun anda\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Dihidupkan\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Utilisasi tepat pada waktu yang direkam\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Pilihan Bahasa\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proses dimulai\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Kunci Publik\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Jam Tenang\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Baca\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Diterima\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Muat ulang\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relasi\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Minta kata sandi sekali pakai\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Minta OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Diperlukan oleh\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Memerlukan\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Reset Kata Sandi\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Diselesaikan\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Restart\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Lanjutkan\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Ganti ulang token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Baris per halaman\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metrik Runtime\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Detail S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Self-Test S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Simpan alamat menggunakan tombol enter atau koma. Biarkan kosong untuk menonaktifkan notifikasi email.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Simpan Pengaturan\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Simpan sistem\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Disimpan di database dan tidak kedaluwarsa sampai Anda menonaktifkannya.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Jadwal\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Jadwalkan jam tenang dimana notifikasi tidak akan dikirim, seperti saat periode pemeliharaan.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Jadwalkan jam tenang dimana notifikasi tidak akan dikirim.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Cari\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Cari sistem atau pengaturan...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Detik di antara ping (default: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Lihat <0>pengaturan notifikasi</0> untuk mengkonfigurasi bagaimana anda menerima peringatan.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Pilih {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Kirim satu ping heartbeat untuk memverifikasi titik akhir Anda berfungsi.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Kirim ping keluar secara berkala ke layanan pemantauan eksternal sehingga Anda dapat memantau Beszel tanpa mengeksposnya ke internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Kirim tes heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Dikirim\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Nomor Seri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Detail Layanan\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Layanan\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Tetapkan ambang persentase untuk warna meter.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Setel variabel lingkungan berikut di hub Beszel Anda untuk mengaktifkan pemantauan heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Pengaturan\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Pengaturan disimpan\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Masuk\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Pengaturan SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Urutkan Berdasarkan\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Waktu Mulai\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Status\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Sub Status\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Ruang swap yang digunakan oleh sistem\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Penggunaan Swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistem\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Rata-rata beban sistem dari waktu ke waktu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Layanan Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistem\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Sistem dapat dikelola dalam file <0>config.yml</0> di dalam direktori data anda.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabel\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tugas\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Unit temperatur\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperatur sensor sistem\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Tes <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Tes heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Notifikasi tes dikirim\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Status keseluruhan adalah <0>ok</0> ketika semua sistem aktif, <1>warn</1> ketika peringatan dipicu, dan <2>error</2> ketika ada sistem yang mati.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Kemudian masuk ke backend dan reset kata sandi akun pengguna anda di tabel pengguna.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Aksi ini tidak dapat di kembalikan. ini akan menghapus permanen semua record {name} dari database\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Ini akan menghapus secara permanen semua record yang dipilih dari database.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Throughput dari {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Throughput dari filesystem root\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Format waktu\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Ke email\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Ganti tampilan\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Ganti tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Token & Fingerprint\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Token memungkinkan agen untuk terhubung dan mendaftar. Fingerprint adalah sistem indentifikasi yang stabil dan unik untuk setiap sistem, diatur pada koneksi pertama.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Token dan Fingerprint digunakan untuk mengautentikasi koneksi WebSocket ke hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Total data yang diterima untuk setiap antarmuka\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Total data yang dikirim untuk setiap antarmuka\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Total: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Dipicu oleh\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Pemicu\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Dipicu ketika rata-rata beban 1 menit melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Dipicu ketika rata-rata beban 15 menit melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Dipicu ketika rata-rata beban 5 menit melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Dipicu ketika sensor apa pun melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Dipicu ketika baterai turun di bawah ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Dipicu ketika up atau down melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Dipicu ketika penggunaan CPU melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Dipicu ketika penggunaan GPU melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Dipicu ketika penggunaan memori melebihi ambang batas\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Dipicu ketika status beralih antara up dan down\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Dipicu ketika penggunaan disk apa pun melebihi ambang batas\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Tipe\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"File unit\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Pengaturan satuan\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token universal\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Tidak diketahui\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Tidak terbatas\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Nyala\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Nyala selama ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Perbarui\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Diperbarui\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Diperbarui setiap 10 menit.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Unggah\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Waktu aktif\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Penggunaan\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Penggunaan partisi root\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Digunakan\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Pengguna\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Nilai\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Lihat\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Lihat lebih banyak\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Lihat 200 peringatan terbaru anda.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Metrik yang Terlihat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Menunggu cukup record untuk ditampilkan\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Ingin membantu meningkatkan terjemahan kami? Periksa <0>Crowdin</0> untuk detail.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Menginginkan\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Peringatan (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Ambang peringatan\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push notifikasi\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Ketika diaktifkan, token ini memungkinkan agen untuk mendaftar sendiri tanpa pembuatan sistem.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Saat menggunakan POST, setiap heartbeat menyertakan payload JSON dengan ringkasan status sistem, daftar sistem yang mati, dan peringatan yang dipicu.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Perintah Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Tulis\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Konfigurasi YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Konfigurasi YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ya\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Pengaturan pengguna anda telah diperbarui.\"\n"
  },
  {
    "path": "internal/site/src/locales/it/it.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: it\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Italian\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: it\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} di {1} righe selezionate.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# core} other {# core}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} giorno} other {{countString} giorni}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} ora} other {{countString} ore}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuto} other {{countString} minuti}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# thread} other {# thread}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 ora\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuto\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 settimana\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 ore\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 ore\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 giorni\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Azioni\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Attivo\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Avvisi Attivi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Stato attivo\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Aggiungi {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Aggiungi <0>Sistema</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Aggiungi sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Aggiungi URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Regola le opzioni di visualizzazione per i grafici.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Regola la larghezza del layout principale\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Amministratore\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Dopo\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Dopo aver impostato le variabili d'ambiente, riavvia il tuo Beszel hub affinché le modifiche abbiano effetto.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agente\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Cronologia Avvisi\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Avvisi\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Tutti i contenitori\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Tutti i Sistemi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Sei sicuro di voler eliminare {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Sei sicuro?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"La copia automatica richiede un contesto sicuro.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Media\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Utilizzo medio della CPU dei container\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"La media scende sotto <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"La media supera <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Consumo energetico medio delle GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Utilizzo medio della CPU a livello di sistema\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Utilizzo medio di {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Utilizzo medio dei motori GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Backup\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Larghezza di banda\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batteria\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Diventato attivo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Diventato inattivo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Prima\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Sotto {0}{1} negli ultimi {2, plural, one {# minuto} other {# minuti}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel supporta OpenID Connect e molti provider di autenticazione OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel utilizza <0>Shoutrrr</0> per integrarsi con i servizi di notifica popolari.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binario\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bit (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Stato di avvio\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Byte (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Può ricaricare\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Può avviare\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Può fermare\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Annulla\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Funzionalità\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capacità\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Attenzione - possibile perdita di dati\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Modifica le unità di visualizzazione per le metriche.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Modifica le opzioni generali dell'applicazione.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Carica\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"In carica\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Opzioni del grafico\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Controlla {email} per un link di reset.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Controlla i log per maggiori dettagli.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Controlla il tuo servizio di monitoraggio\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Controlla il tuo servizio di notifica\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Cancella\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Fare clic su un contenitore per visualizzare ulteriori informazioni.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Fare clic su un dispositivo per visualizzare più informazioni.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Clicca su un sistema per visualizzare più informazioni.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Clicca per copiare\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Istruzioni da riga di comando\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configura come ricevere le notifiche di avviso.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Conferma password\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflitti\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"La connessione è interrotta\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Continua\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Copiato negli appunti\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Copia docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Copia docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Copia env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Copia host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Copia comando Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Copia nome\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Copia testo\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Copia il comando di installazione per l'agente qui sotto, o registra gli agenti automaticamente con un <0>token universale</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Copia il contenuto<0>docker-compose.yml</0> per l'agente qui sotto, o registra gli agenti automaticamente con un <1>token universale</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Copia YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Core CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Picco CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Tempo CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Suddivisione tempo CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Utilizzo CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Crea\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Crea account\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Creato\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Critico (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Download cumulativo\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Upload cumulativo\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Stato attuale\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cicli\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Giornaliero\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Periodo di tempo predefinito\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Elimina\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Elimina impronta digitale\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Descrizione\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Dettagli\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Dispositivo\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"In scarica\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"I/O Disco\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Unità disco\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Utilizzo Disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Utilizzo del disco di {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Utilizzo CPU Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Utilizzo Memoria Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"I/O di Rete Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentazione\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Offline\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Offline ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Scarica\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Durata\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Modifica\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Modifica {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Notifiche email\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Vuota\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Ora di fine\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL dell'endpoint\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL dell'endpoint da pingare (richiesto)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Inserisci l'indirizzo email per reimpostare la password\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Inserisci l'indirizzo email...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Inserisci la tua password monouso.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Effimero\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Errore\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Esempio:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Supera {0}{1} negli ultimi {2, plural, one {# minuto} other {# minuti}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID principale exec\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"I sistemi esistenti non definiti in <0>config.yml</0> verranno eliminati. Si prega di effettuare backup regolari.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Uscito attivo\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Scade dopo un'ora o al riavvio dell'hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Esporta\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Esporta configurazione\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Esporta la configurazione attuale dei tuoi sistemi.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Fallito\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Attributi falliti:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Autenticazione fallita\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Salvataggio delle impostazioni fallito\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Invio heartbeat fallito\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Invio della notifica di test fallito\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Aggiornamento dell'avviso fallito\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Fallito: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtra...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Impronta digitale\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Per <0>{min}</0> {min, plural, one {minuto} other {minuti}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Password dimenticata?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Comando FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Piena\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Generale\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globale\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Motori GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Consumo della GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Utilizzo GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Griglia\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Stato\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitoraggio Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat inviato con successo\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Comando Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Metodo HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Metodo HTTP: POST, GET o HEAD (predefinito: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inattiva\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Se hai perso la password del tuo account amministratore, puoi reimpostarla utilizzando il seguente comando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Immagine\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inattivo\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervallo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Indirizzo email non valido.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Lingua\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Aspetto\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Larghezza del layout\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Ciclo di vita\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limite\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Carico medio\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Caricamento medio 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Caricamento medio 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Caricamento medio 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Carico Medio\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Stato di caricamento\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Caricamento...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Disconnetti\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Accedi\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Tentativo di accesso fallito\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Log\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Cerchi invece dove creare avvisi? Clicca sulle icone della campana <0/> nella tabella dei sistemi.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID principale\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Gestisci le preferenze di visualizzazione e notifica.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Istruzioni di configurazione manuale\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memoria\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Limite memoria\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Picco memoria\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Utilizzo Memoria\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Utilizzo della memoria dei container Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modello\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nome\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Rete\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Traffico di rete dei container Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Traffico di rete delle interfacce pubbliche\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Unità rete\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"No\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nessun risultato trovato.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Nessun risultato.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Nessun attributo S.M.A.R.T. disponibile per questo dispositivo.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nessun sistema trovato.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notifiche\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Supporto OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Ad ogni riavvio, i sistemi nel database verranno aggiornati per corrispondere ai sistemi definiti nel file.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Una volta\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Password monouso\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Apri menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Oppure continua con\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Altro\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Sovrascrivi avvisi esistenti\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Pagina\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Pagina {0} di {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Pagine / Impostazioni\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Password\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"La password deve contenere almeno 8 caratteri.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"La password deve essere inferiore a 72 byte.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Richiesta di reimpostazione password ricevuta\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Passato\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pausa\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"In pausa\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"In pausa ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Formato del payload\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Utilizzo medio per core\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Percentuale di tempo trascorso in ogni stato\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanente\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistenza\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Si prega di <0>configurare un server SMTP</0> per garantire la consegna degli avvisi.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Si prega di controllare i log per maggiori dettagli.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Si prega di controllare le credenziali e riprovare\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Si prega di creare un account amministratore\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Si prega di abilitare i pop-up per questo sito\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Si prega di accedere nuovamente\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Si prega di consultare <0>la documentazione</0> per le istruzioni.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Si prega di accedere al proprio account\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Porta\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Accensione\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Utilizzo preciso al momento registrato\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Lingua Preferita\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Processo avviato\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Chiave Pub\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Ore silenziose\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lettura\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Ricevuto\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Aggiorna\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relazioni\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Richiedi una password monouso\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Richiedi OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Richiesto da\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Richiede\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Reimposta Password\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Risolto\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Riavvii\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Riprendi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Ruota token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Righe per pagina\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metriche di runtime\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Dettagli S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Autotest S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Salva l'indirizzo usando il tasto invio o la virgola. Lascia vuoto per disabilitare le notifiche email.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Salva Impostazioni\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Salva sistema\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Salvato nel database e non scade finché non lo disabiliti.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Pianifica\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Pianifica le ore silenziose in cui le notifiche non verranno inviate, ad esempio durante i periodi di manutenzione.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Pianifica le ore silenziose in cui le notifiche non verranno inviate.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Cerca\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Cerca sistemi o impostazioni...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Secondi tra i ping (predefinito: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Vedi <0>impostazioni di notifica</0> per configurare come ricevere gli avvisi.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Seleziona {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Invia un singolo ping di heartbeat per verificare che l'endpoint funzioni.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Invia ping in uscita periodici a un servizio di monitoraggio esterno in modo da poter monitorare Beszel senza esporlo a Internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Invia heartbeat di prova\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Inviato\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Numero di serie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Dettagli servizio\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Servizi\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Imposta le soglie percentuali per i colori dei contatori.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Imposta le seguenti variabili d'ambiente sul tuo Beszel hub per abilitare il monitoraggio heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Impostazioni\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Impostazioni salvate\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Accedi\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Impostazioni SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Ordina per\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Ora di inizio\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Stato\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Stato\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Sotto-stato\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Spazio di swap utilizzato dal sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Utilizzo Swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Medie di carico del sistema nel tempo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Servizi Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistemi\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"I sistemi possono essere gestiti in un file <0>config.yml</0> all'interno della tua directory dati.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabella\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Attività\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Unità temperatura\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperature dei sensori di sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Notifica di test inviata\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Lo stato generale è <0>ok</0> quando tutti i sistemi sono attivi, <1>avviso</1> quando gli avvisi sono attivati e <2>errore</2> quando un sistema è inattivo.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Quindi accedi al backend e reimposta la password del tuo account utente nella tabella degli utenti.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Questa azione non può essere annullata. Questo eliminerà permanentemente tutti i record attuali per {name} dal database.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Questo eliminerà permanentemente tutti i record selezionati dal database.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Throughput di {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Throughput del filesystem root\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Formato orario\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"A email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Attiva/disattiva griglia\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Attiva/disattiva tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Token e Impronte Digitali\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"I token consentono agli agenti di connettersi e registrarsi. Le impronte digitali sono identificatori stabili unici per ogni sistema, impostati alla prima connessione.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"I token e le impronte digitali vengono utilizzati per autenticare le connessioni WebSocket all'hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Totale\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Dati totali ricevuti per ogni interfaccia\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Dati totali inviati per ogni interfaccia\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Totale: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Attivato da\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Trigger\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Si attiva quando la media di carico di 1 minuto supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Si attiva quando la media di carico di 15 minuti supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Si attiva quando la media di carico di 5 minuti supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Attiva quando un sensore supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Attiva quando la carica della batteria scende sotto una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Attiva quando il combinato up/down supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Attiva quando l'utilizzo della CPU supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Si attiva quando l'utilizzo della GPU supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Attiva quando l'utilizzo della memoria supera una soglia\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Attiva quando lo stato passa tra up e down\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Attiva quando l'utilizzo di un disco supera una soglia\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Tipo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"File unit\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Preferenze unità\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token universale\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Sconosciuta\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Illimitato\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Attivo\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Attivo ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Aggiorna\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Aggiornato\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Aggiornato ogni 10 minuti.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Carica\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Utilizzo\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Utilizzo della partizione root\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Utilizzato\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Utenti\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Valore\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Vista\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Visualizza altro\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Visualizza i tuoi 200 avvisi più recenti.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Colonne visibili\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"In attesa di abbastanza record da visualizzare\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Vuoi aiutarci a migliorare ulteriormente le nostre traduzioni? Dai un'occhiata a <0>Crowdin</0> per maggiori dettagli.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Desidera\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Avviso (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Soglie di avviso\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Notifiche Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Quando abilitato, questo token consente agli agenti di registrarsi automaticamente senza creazione preventiva del sistema.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Quando si usa POST, ogni heartbeat include un payload JSON con il riepilogo dello stato del sistema, l'elenco dei sistemi inattivi e gli avvisi attivati.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Comando Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Scrittura\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Configurazione YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Configurazione YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Sì\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Le impostazioni utente sono state aggiornate.\"\n"
  },
  {
    "path": "internal/site/src/locales/ja/ja.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ja\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Japanese\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: ja\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{1}行のうち{0}行が選択されました。\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# コア} other {# コア}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} 日} other {{countString} 日}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} 時間} other {{countString} 時間}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} 分} few {{countString} 分} many {{countString} 分} other {{countString} 分}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# スレッド} other {# スレッド}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1時間\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1分\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1分\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1週間\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12時間\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15分\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24時間\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30日間\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5分\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"アクション\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"アクティブ\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"アクティブなアラート\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"アクティブ状態\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"{foo}を追加\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"<0>システム</0>を追加\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"システムを追加\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"URLを追加\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"チャートの表示オプションを調整します。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"メインレイアウトの幅を調整\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"管理者\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"後\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"環境変数を設定した後、変更を有効にするために Beszel ハブを再起動してください。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"エージェント\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"アラート履歴\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"アラート\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"すべてのコンテナ\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"すべてのシステム\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"{name}を削除してもよろしいですか？\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"よろしいですか？\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"自動コピーには安全なコンテキストが必要です。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"平均\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"コンテナの平均CPU使用率\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"平均が<0>{value}{0}</0>を下回っています\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"平均が<0>{value}{0}</0>を超えています\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPUの平均消費電力\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"システム全体の平均CPU使用率\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0}の平均使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPUエンジンの平均使用率\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"バックアップ\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"帯域幅\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"バッテリー\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"バッテリー\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"アクティブになった\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"非アクティブになった\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"前\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"過去{2, plural, one {# 分} other {# 分}}で{0}{1}を下回っています\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"BeszelはOpenID Connectと多くのOAuth2認証プロバイダーをサポートしています。\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszelは<0>Shoutrrr</0>を使用して、人気のある通知サービスと統合します。\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"バイナリ\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"ビット (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"ブート状態\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"バイト (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"キャッシュ / バッファ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"リロード可能\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"開始可能\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"停止可能\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"キャンセル\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"機能\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"容量\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"注意 - データ損失の可能性\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"摂氏 (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"メトリックの表示単位を変更します。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"一般的なアプリケーションオプションを変更します。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"充電\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"充電中\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"チャートオプション\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"{email}を確認してリセットリンクを探してください。\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"詳細についてはログを確認してください。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"監視サービスを確認する\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"通知サービスを確認してください\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"クリア\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"詳細情報を表示するにはコンテナをクリックしてください。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"詳細情報を表示するにはデバイスをクリックしてください。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"システムをクリックして詳細を表示します。\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"クリックしてコピー\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"コマンドラインの指示\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"アラート通知の受信方法を設定します。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"パスワードを確認\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"競合\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"接続が切断されました\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"続行\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"クリップボードにコピーされました\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"docker compose をコピー\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"docker run をコピー\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"環境変数をコピー\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"ホストをコピー\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Linuxコマンドをコピー\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"名前をコピーする\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"テキストをコピー\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"下記のエージェントのインストールコマンドをコピーするか、<0>ユニバーサルトークン</0>を使用してエージェントを自動登録してください。\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"下記のエージェントの<0>docker-compose.yml</0>内容をコピーするか、<1>ユニバーサルトークン</1>を使用してエージェントを自動登録してください。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAMLをコピー\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU コア\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPUピーク\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU時間\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU 時間の内訳\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU使用率\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"作成\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"アカウントを作成\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"作成日\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"致命的 (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"累積ダウンロード\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"累積アップロード\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"現在の状態\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"サイクル\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"毎日\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"デフォルトの期間\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"削除\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"フィンガープリントを削除\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"説明\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"詳細\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"デバイス\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"放電中\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"ディスク\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"ディスクI/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"ディスク単位\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"ディスク使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName}のディスク使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Dockerメモリ使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"DockerネットワークI/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"ドキュメント\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"停止\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"停止 ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"ダウンロード\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"期間\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"編集\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"{foo}を編集\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"メール\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"メール通知\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"空\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"終了時間\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"エンドポイント URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"ping するエンドポイント URL (必須)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"パスワードをリセットするためにメールアドレスを入力してください\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"メールアドレスを入力...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"ワンタイムパスワードを入力してください。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"一時的\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"エラー\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"例:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"過去{2, plural, one {# 分} other {# 分}}で{0}{1}を超えています\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"実行メインPID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"<0>config.yml</0>に定義されていない既存のシステムは削除されます。定期的にバックアップを作成してください。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"アクティブ状態で終了\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"1時間後、またはハブの再起動時に有効期限が切れます。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"エクスポート\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"設定をエクスポート\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"現在のシステム設定をエクスポートします。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"華氏 (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"失敗\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"失敗した属性:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"認証に失敗しました\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"設定の保存に失敗しました\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"ハートビートの送信に失敗しました\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"テスト通知の送信に失敗しました\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"アラートの更新に失敗しました\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"失敗: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"フィルター...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"フィンガープリント\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"ファームウェア\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"<0>{min}</0> {min, plural, one {分} other {分}}の間\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"パスワードをお忘れですか？\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD コマンド\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"満充電\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"一般\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"グローバル\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPUエンジン\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPUの消費電力\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU使用率\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"グリッド\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"ヘルス\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"ハートビート\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"ハートビート監視\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"ハートビートが正常に送信されました\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew コマンド\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"ホスト / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP メソッド\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP メソッド: POST、GET、または HEAD (デフォルト: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"アイドル\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"管理者アカウントのパスワードを忘れた場合は、次のコマンドを使用してリセットできます。\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"イメージ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"非アクティブ\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"間隔\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"無効なメールアドレスです。\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"言語\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"レイアウト\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"レイアウト幅\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"ライフサイクル\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"制限\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"負荷平均\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"負荷平均 (15分)\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"負荷平均 (1分)\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"負荷平均 (5分)\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"負荷平均\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"ロード状態\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"読み込み中...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"ログアウト\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"ログイン\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"ログイン試行に失敗しました\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"ログ\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"アラートを作成する場所を探していますか？システムテーブルのベル<0/>アイコンをクリックしてください。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"メインPID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"表示と通知の設定を管理します。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"手動セットアップの手順\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"最大1分\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"メモリ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"メモリ制限\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"メモリピーク\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"メモリ使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Dockerコンテナのメモリ使用率\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"モデル\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"名前\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"帯域\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Dockerコンテナのネットワークトラフィック\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"パブリックインターフェースのネットワークトラフィック\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"ネットワーク単位\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"いいえ\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"結果が見つかりませんでした。\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"結果がありません。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"このデバイスのS.M.A.R.T.属性は利用できません。\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"システムが見つかりませんでした。\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"通知\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDCサポート\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"再起動のたびに、データベース内のシステムはファイルに定義されたシステムに一致するように更新されます。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"1回限り\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"ワンタイムパスワード\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"メニューを開く\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"または、以下の方法でログイン\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"その他\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"既存のアラートを上書き\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"ページ\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"{1}ページ中{0}ページ目\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"ページ / 設定\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"パスワード\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"パスワードは8文字以上である必要があります。\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"パスワードは72バイト未満でなければなりません。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"パスワードリセットのリクエストを受け取りました\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"過去\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"一時停止\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"一時停止中\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"一時停止 ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"ペイロード形式\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"コアごとの平均使用率\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"各状態で費やした時間の割合\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"永久\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"永続性\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"アラートが配信されるように<0>SMTPサーバーを設定</0>してください。\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"詳細についてはログを確認してください。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"資格情報を確認して再試行してください\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"管理者アカウントを作成してください\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"このサイトのポップアップを有効にしてください\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"再度ログインしてください\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"手順については<0>ドキュメント</0>を参照してください。\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"アカウントにサインインしてください\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"ポート\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"電源オン\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"記録された時点での正確な利用\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"優先言語\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"プロセス開始\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"公開鍵\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"サイレント時間\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"読み取り\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"受信\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"更新\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"関係\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"ワンタイムパスワードをリクエスト\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP をリクエスト\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"必要とされる\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"必要とする\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"パスワードをリセット\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"解決済み\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"再起動\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"再開\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"ルート\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"トークンをローテート\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"ページあたりの行数\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"ランタイムメトリクス\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.詳細\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T.セルフテスト\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Enterキーまたはカンマを使用してアドレスを保存します。空白のままにするとメール通知が無効になります。\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"設定を保存\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"システムを保存\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"データベースに保存され、無効にするまで有効期限が切れません。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"スケジュール\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"メンテナンス期間中などの通知が送信されないサイレント時間をスケジュールします。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"通知が送信されないサイレント時間をスケジュールします。\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"検索\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"システムまたは設定を検索...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"ping 間の秒数 (デフォルト: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"アラートの受信方法を設定するには<0>通知設定</0>を参照してください。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"{foo}を選択\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"エンドポイントが機能していることを確認するために、単一のハートビート ping を送信します。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"外部監視サービスに定期的にアウトバウンド ping を送信することで、Beszel をインターネットに公開せずに監視できます。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"テストハートビートを送信\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"送信\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"シリアル番号\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"サービス詳細\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"サービス\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"メーターの色を変更するしきい値（パーセンテージ）を設定します。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"ハートビート監視を有効にするには、Beszel ハブで次の環境変数を設定します。\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"設定\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"設定が保存されました\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"サインイン\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP設定\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"並び替え基準\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"開始時間\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"状態\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"ステータス\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"サブ状態\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"システムが使用するスワップ領域\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"スワップ使用量\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"システム\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"システムの負荷平均の推移\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemdサービス\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"システム\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"システムはデータディレクトリ内の<0>config.yml</0>ファイルで管理できます。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"テーブル\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"タスク\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"温度\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"温度\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"温度単位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"システムセンサーの温度\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"テスト<0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"テストハートビート\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"テスト通知が送信されました\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"全体的なステータスは、すべてのシステムが稼働している場合は <0>ok</0>、アラートがトリガーされた場合は <1>警告</1>、いずれかのシステムがダウンしている場合は <2>エラー</2> になります。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"その後、バックエンドにログインして、ユーザーテーブルでユーザーアカウントのパスワードをリセットしてください。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"この操作は元に戻せません。これにより、データベースから{name}のすべての現在のレコードが永久に削除されます。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"これにより、選択したすべてのレコードがデータベースから完全に削除されます。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName}のスループット\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"ルートファイルシステムのスループット\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"時間形式\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"宛先メールアドレス\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"グリッドを切り替え\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"テーマを切り替え\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"トークン\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"トークンとフィンガープリント\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"トークンはエージェントの接続と登録を可能にします。フィンガープリントは各システム固有の安定した識別子で、初回接続時に設定されます。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"トークンとフィンガープリントは、ハブへのWebSocket接続の認証に使用されます。\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"総数\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"各インターフェースの総受信データ量\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"各インターフェースの総送信データ量\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"合計: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"トリガー元\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"トリガー\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"1分間の負荷平均がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"15分間の負荷平均がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"5分間の負荷平均がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"センサーがしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"バッテリーの充電量がしきい値を下回ったときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"上り/下りの合計がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"CPU使用率がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"GPU使用率がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"メモリ使用率がしきい値を超えたときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"ステータスが上から下に切り替わるときにトリガーされます\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"ディスクの使用量がしきい値を超えたときにトリガーされます\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"タイプ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"ユニットファイル\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"単位の設定\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"ユニバーサルトークン\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"不明\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"無制限\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"正常\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"正常 ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"更新\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"更新済み\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"10分ごとに更新されます。\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"アップロード\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"稼働時間\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"ルートパーティションの使用量\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"使用中\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"ユーザー\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"値\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"表示\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"もっと見る\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"直近200件のアラートを表示します。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"表示列\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"表示するのに十分なレコードを待っています\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"翻訳をさらに良くするためにご協力をお願いします。詳細については<0>Crowdin</0>をご覧ください。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"要求\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"警告 (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"警告のしきい値\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / プッシュ通知\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"有効にすると、このトークンによりエージェントは事前のシステム作成なしで自己登録できます。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"POST を使用する場合、各ハートビートには、システムステータスの概要、ダウンしているシステムのリスト、およびトリガーされたアラートを含む JSON ペイロードが含まれます。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows コマンド\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"書き込み\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML設定\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML設定\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"はい\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"ユーザー設定が更新されました。\"\n"
  },
  {
    "path": "internal/site/src/locales/ko/ko.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ko\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Korean\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: ko\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{1}개의 행 중 {0}개가 선택되었습니다.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# 코어} other {# 코어}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} 일} other {{countString} 일}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} 시간} other {{countString} 시간}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} 분} few {{countString} 분} many {{countString} 분} other {{countString} 분}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# 스레드} other {# 스레드}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1시간\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1분\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1분\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1주\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12시간\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15분\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24시간\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30일\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5분\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"작업\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"활성\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"활성화된 알림들\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"활성 상태\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"{foo} 추가\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"<0>시스템</0> 추가\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"시스템 추가\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"URL 추가\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"차트 표시 옵션 변경.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"메인 레이아웃 너비 조정\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"관리자\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"이후\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"환경 변수를 설정한 후, 변경 사항을 적용하려면 Beszel 허브를 재시작하세요.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"에이전트\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"알림 기록\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"알림\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"모든 컨테이너\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"모든 시스템\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"{name}을(를) 삭제하시겠습니까?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"확실합니까?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"자동 복사는 안전한 컨텍스트가 필요합니다.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"평균\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Docker 컨테이너의 평균 CPU 사용량\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"평균이 <0>{value}{0}</0> 아래로 떨어집니다\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"평균이 <0>{value}{0}</0>을(를) 초과합니다\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU들의 평균 전원 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"시스템 전체의 평균 CPU 사용량\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"평균 {0} 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU 엔진 평균 사용량\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"백업\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"대역폭\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"배터리\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"배터리\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"활성화됨\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"비활성화됨\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"이전\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"마지막 {2, plural, one {# 분} other {# 분}} 동안 {0}{1} 미만\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel은 OpenID Connect 및 많은 OAuth2 인증 제공자를 지원합니다.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel은 여러 인기 있는 알림 서비스와 연동하기 위해 <0>Shoutrrr</0>을 이용합니다.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"실행 파일\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"비트 (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"부팅 상태\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"바이트 (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"캐시 / 버퍼\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"재로드 가능\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"시작 가능\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"중지 가능\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"취소\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"권한\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"용량\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"주의 - 데이터 손실 가능성\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"섭씨 (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"메트릭의 표시 단위를 변경합니다.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"일반 애플리케이션 옵션 변경.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"충전\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"충전 중\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"차트 옵션\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"{email}에서 재설정 링크를 확인하세요.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"자세한 내용은 로그를 확인하세요.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"모니터링 서비스 확인\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"알림 서비스를 확인하세요.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"지우기\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"더 많은 정보를 보려면 컨테이너를 클릭하세요.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"더 많은 정보를 보려면 장치를 클릭하세요.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"더 많은 정보를 보려면 시스템을 클릭하세요.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"클릭하여 복사\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"명령어 사용 지침\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"알림을 수신할 방법을 설정하세요.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"비밀번호 확인\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"충돌\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"연결이 끊겼습니다\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"계속\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"클립보드에 복사됨\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"docker compose 내용 복사\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"docker run 명령어 복사\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"환경 복사\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"호스트 복사\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"리눅스 명령어 복사\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"이름 복사\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"텍스트 복사\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"아래 에이전트의 설치 명령을 복사하거나 <0>범용 토큰</0>으로 에이전트를 자동으로 등록하세요.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"아래 에이전트의 <0>docker-compose.yml</0> 내용을 복사하거나 <1>범용 토큰</1>으로 에이전트를 자동으로 등록하세요.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAML 복사\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU 코어\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU 최대값\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU 시간\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU 시간 분배\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU 사용량\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"생성\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"계정 생성\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"생성됨\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"위험 (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"누적 다운로드\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"누적 업로드\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"현재 상태\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"사이클\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"매일\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"기본 기간\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"삭제\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"지문 삭제\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"설명\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"세부사항\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"장치\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"방전 중\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"디스크\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"디스크 I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"디스크 단위\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"디스크 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName}의 디스크 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker 메모리 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker 네트워크 I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"문서\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"오프라인\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"오프라인 ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"다운로드\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"기간\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"수정\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"{foo} 수정\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"이메일\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"이메일 알림\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"빔\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"종료 시간\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"엔드포인트 URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"핑을 보낼 엔드포인트 URL (필수)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"비밀번호를 재설정하려면 이메일 주소를 입력하세요\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"이메일 주소 입력...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"OTP를 입력하세요.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"일시적\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"오류\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"예시:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"마지막 {2, plural, one {# 분} other {# 분}} 동안 {0}{1} 초과\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"실행 메인 PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"<0>config.yml</0>에 정의되지 않은 기존 시스템은 삭제됩니다. 정기적으로 백업을 하세요.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"활성 종료됨\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"한 시간 후 또는 허브 재시작 시 만료됩니다.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"내보내기\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"구성 내보내기\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"현재 시스템 구성 내보내기\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"화씨 (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"실패\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"실패한 속성:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"인증 실패\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"설정 저장 실패\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"하트비트 전송 실패\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"테스트 알림 전송 실패\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"알림 수정 실패\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"실패: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"필터...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"지문\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"펌웨어\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"<0>{min}</0> {min, plural, one {분} other {분}} 동안\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"비밀번호를 잊으셨나요?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD 명령어\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"가득\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"일반\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"전역\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU 엔진들\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU 전원 사용량\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU 사용량\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"그리드\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"상태\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"하트비트\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"하트비트 모니터링\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"하트비트 전송 성공\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew 명령어\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"호스트 / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP 메서드\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP 메서드: POST, GET 또는 HEAD (기본값: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"대기\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"관리자 계정의 비밀번호를 잃어버린 경우, 다음 명령어를 사용하여 재설정할 수 있습니다.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"이미지\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"비활성\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"간격\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"잘못된 이메일 주소입니다.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"언어\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"레이아웃\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"레이아웃 너비\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"생명주기\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"제한\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"부하 평균\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"부하 평균 15분\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"부하 평균 1분\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"부하 평균 5분\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"부하 평균\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"로드 상태\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"로딩 중...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"로그아웃\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"로그인\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"로그인 실패\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"로그\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"알림을 생성하려 하시나요? 시스템 테이블의 종 <0/> 아이콘을 클릭하세요.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"메인 PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"디스플레이 및 알림 설정\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"수동 설정 방법\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"1분간 최댓값\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"메모리\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"메모리 제한\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"메모리 최대값\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"메모리 사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker 컨테이너의 메모리 사용량\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"모델\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"이름\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"네트워크\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker 컨테이너의 네트워크 트래픽\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"공용 인터페이스의 네트워크 트래픽\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"네트워크 단위\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"아니오\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"결과가 없습니다.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"결과 없음.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"이 장치에 사용할 수 있는 S.M.A.R.T. 속성이 없습니다.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"시스템을 찾을 수 없습니다.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"알림\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC 지원\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"매 시작 시, 데이터베이스가 파일에 정의된 시스템과 일치하도록 업데이트됩니다.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"일회성\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"OTP\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"메뉴 열기\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"또는 아래 항목으로 진행하기\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"기타\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"기존 알림 덮어쓰기\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"페이지\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"{1}페이지 중 {0}페이지\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"페이지 / 설정\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"비밀번호\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"비밀번호는 최소 8자 이상이어야 합니다.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"비밀번호는 72 바이트 이하여야 합니다.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"비밀번호 재설정 요청이 접수되었습니다\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"과거\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"일시 중지\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"일시 정지됨\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"일시 정지됨 ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"페이로드 형식\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"코어별 평균 사용률\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"각 상태에서 보낸 시간의 백분율\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"영구적\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"지속성\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"알림이 전달되도록 <0>SMTP 서버를 구성</0>하세요.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"자세한 내용은 로그를 확인하세요.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"자격 증명을 확인하고 다시 시도하세요.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"관리자 계정을 생성하세요.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"이 사이트에 대해 팝업을 활성화하세요.\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"다시 로그인하세요.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"사용법은 <0>문서</0>를 참조하세요.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"계정에 로그인하세요.\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"포트\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"전원 켜기\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"기록된 시간의 정확한 사용량\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"선호 언어\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"프로세스 시작됨\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"공개 키\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"조용한 시간\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"읽기\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"수신됨\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"새로고침\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"관계\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"OTP 요청\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP 요청\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"필요한 대상\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"필요 항목\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"비밀번호 재설정\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"해결됨\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"재시작 횟수\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"재개\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"루트\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"토큰 회전\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"페이지당 행 수\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"런타임 메트릭\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. 세부 정보\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. 자체 테스트\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Enter 키 또는 쉼표를 사용하여 주소를 저장하세요. 이메일 알림을 비활성화하려면 비워 두세요.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"설정 저장\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"시스템 저장\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"데이터베이스에 저장되며 비활성화할 때까지 만료되지 않습니다.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"일정\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"유지보수 기간 등 알림이 전송되지 않을 조용한 시간을 예약하세요.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"알림이 전송되지 않을 조용한 시간을 예약하세요.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"검색\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"시스템 또는 설정 검색...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"핑 사이 시간(초) (기본값: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"알림을 받는 방법을 구성하려면 <0>알림 설정</0>을 참조하세요.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"{foo} 선택\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"엔드포인트가 작동하는지 확인하기 위해 단일 하트비트 핑을 보냅니다.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"외부 모니터링 서비스에 주기적으로 아웃바운드 핑을 보내 인터넷에 노출하지 않고도 Beszel을 모니터링할 수 있습니다.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"테스트 하트비트 전송\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"보냄\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"시리얼 번호\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"서비스 세부 정보\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"서비스\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"그래프 미터 색상의 백분율 임계값을 설정합니다.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"하트비트 모니터링을 활성화하려면 Beszel 허브에 다음 환경 변수를 설정하세요:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"설정\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"설정이 저장되었습니다.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"로그인\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP 설정\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"정렬 기준\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"시작 시간\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"상태\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"상태\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"하위 상태\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"시스템에서 사용된 스왑 공간\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"스왑 사용량\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"시스템\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"시간에 따른 시스템 부하 평균\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd 서비스\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"시스템\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"시스템은 데이터 디렉토리 내의 <0>config.yml</0> 파일에서 관리할 수 있습니다.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"표\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"작업\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"온도\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"온도\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"온도 단위\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"시스템 센서의 온도\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"테스트 <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"테스트 하트비트\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"테스트 알림이 전송되었습니다.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"모든 시스템이 정상이면 <0>ok</0>, 알림이 트리거되면 <1>경고</1>, 시스템이 다운되면 <2>오류</2> 상태가 됩니다.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"그런 다음 백엔드에 로그인하여 사용자 테이블에서 사용자 계정 비밀번호를 재설정하세요.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"이 작업은 되돌릴 수 없습니다. 데이터베이스에서 {name}에 대한 모든 현재 기록이 영구적으로 삭제됩니다.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"선택한 모든 레코드를 데이터베이스에서 영구적으로 삭제합니다.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName}의 처리량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"루트 파일 시스템의 처리량\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"시간 형식\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"받는사람(들)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"그리드 전환\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"테마 전환\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"토큰\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"토큰 및 지문\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"토큰은 에이전트가 연결하고 등록할 수 있도록 합니다. 지문은 첫 연결 시 설정되는 각 시스템의 고유한 안정적인 식별자입니다.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"토큰과 지문은 허브에 대한 WebSocket 연결을 인증하는 데 사용됩니다.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"총\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"각 인터페이스별 총합 다운로드 데이터량\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"각 인터페이스별 총합 업로드 데이터량\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"총: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"트리거 대상\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"트리거\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"1분 부하 평균이 임계값을 초과하면 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"15분 부하 평균이 임계값을 초과하면 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"5분 부하 평균이 임계값을 초과하면 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"센서가 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"배터리 충전량이 임계값 아래로 떨어질 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"업로드와 다운로드 대역폭의 합이 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"CPU 사용량이 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"GPU 사용량이 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"메모리 사용량이 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"시스템의 전원이 켜지거나 꺼질때 트리거됩니다.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"디스크 사용량이 임계값을 초과할 때 트리거됩니다.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"유형\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"유닛 파일\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"단위 기본 설정\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"범용 토큰\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"알 수 없음\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"무제한\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"온라인\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"온라인 ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"업데이트\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"업데이트됨\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"10분마다 업데이트됩니다.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"업로드\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"가동시간\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"사용량\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"루트 파티션의 사용량\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"사용됨\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"사용자\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"값\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"보기\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"더 보기\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"최근 200개의 알림을 봅니다.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"표시할 열\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"표시할 충분한 기록을 기다리는 중\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"번역을 개선하는데 도움을 주시겠습니까? 자세한 내용은 <0>Crowdin</0>을 확인해 주세요.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"요구 항목\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"경고 (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"경고 임계값\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / 푸시 알림\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"활성화되면 이 토큰은 사전 시스템 생성 없이 에이전트가 자체 등록할 수 있도록 합니다.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"POST를 사용할 때 각 하트비트에는 시스템 상태 요약, 다운된 시스템 목록 및 트리거된 알림이 포함된 JSON 페이로드가 포함됩니다.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows 명령어\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"쓰기\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML 구성\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML 구성\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"예\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"사용자 설정이 업데이트되었습니다.\"\n"
  },
  {
    "path": "internal/site/src/locales/nl/nl.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: nl\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-02-19 19:40\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Dutch\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: nl\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} van de {1} rij(en) geselecteerd.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# kern} other {# kernen}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dag} other {{countString} dagen}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} uur} other {{countString} uren}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuut} other {{countString} minuten}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# thread} other {# threads}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 uur\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 minuut\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuut\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 Maand\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 uren\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 minuten\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 uren\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dagen\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 minuten\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Acties\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Actief\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Actieve waarschuwingen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Actieve status\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Voeg {foo} toe\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Voeg <0>Systeem</0> toe\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Voeg systeem toe\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Voeg URL toe\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Weergaveopties voor grafieken aanpassen.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Breedte van het hoofdlayout aanpassen\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Administrator\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Na\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Start na het instellen van de omgevingsvariabelen je Beszel-hub opnieuw op om de wijzigingen door te voeren.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Melding geschiedenis\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Waarschuwingen\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Alle containers\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Alle systemen\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Weet je zeker dat je {name} wilt verwijderen?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Weet je het zeker?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatisch kopiëren vereist een veilige context.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Gemiddelde\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Gemiddeld CPU-gebruik van containers\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Gemiddelde daalt onder <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Gemiddelde overschrijdt <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Gemiddeld stroomverbruik van GPU's\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Gemiddeld systeembrede CPU-gebruik\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Gemiddeld gebruik van {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Gemiddeld gebruik van GPU-engines\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Back-ups\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandbreedte\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batterij\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Actief geworden\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Inactief geworden\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Voor\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Onder {0}{1} in de laatste {2, plural, one {# minuut} other {# minuten}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel ondersteunt OpenID Connect en vele OAuth2 authenticatieaanbieders.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel gebruikt <0>Shoutrr</0> om te integreren met populaire meldingsdiensten.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binair\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Opstartstatus\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffers\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Kan herladen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Kan starten\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Kan stoppen\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Annuleren\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Mogelijkheden\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capaciteit\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Opgelet - potentieel gegevensverlies\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Verander statistiek eenheden.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Wijzig algemene applicatie opties.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Lading\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Opladen\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Grafiekopties\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Controleer {email} op een reset link.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Controleer de logs voor meer details.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Controleer je monitoringservice\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Controleer je meldingsservice\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Wissen\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klik op een container om meer informatie te zien.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klik op een apparaat om meer informatie te bekijken.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klik op een systeem om meer informatie te bekijken.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klik om te kopiëren\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instructies voor de opdrachtregel\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configureer hoe je waarschuwingsmeldingen ontvangt.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Bevestig wachtwoord\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflicten\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Verbinding is niet actief\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Volgende\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Gekopieerd naar het klembord\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Docker compose kopiëren\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Docker run kopiëren\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Env kopiëren\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopieer host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopieer Linux-opdracht\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopieer naam\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopieer tekst\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopieer de installatie opdracht voor de agent hieronder, of registreer agenten automatisch met een <0>universele token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopieer de<0>docker-compose.yml</0> inhoud voor de agent hieronder, of registreer agenten automatisch met een <1>universele token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAML kopiëren\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU-kernen\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU-piek\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU-tijd\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU-tijdverdeling\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Processorgebruik\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Aanmaken\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Account aanmaken\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Aangemaakt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritiek (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Cumulatieve download\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Cumulatieve upload\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Huidige status\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cycli\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Dagelijks\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standaard tijdsduur\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Verwijderen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Vingerafdruk verwijderen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Beschrijving\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Details\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Apparaat\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Ontladen\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Schijf\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Schijf I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Schijf eenheid\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Schijfgebruik\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Schijfgebruik van {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU-gebruik\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker geheugengebruik\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker netwerk I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentatie\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Offline\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Offline ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Downloaden\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Duur\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Bewerken\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Bewerk {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-mail\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-mailnotificaties\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Leeg\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Eindtijd\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Endpoint-URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Endpoint-URL om te pingen (vereist)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Voer een e-mailadres in om het wachtwoord opnieuw in te stellen\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Voer een e-mailadres in...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Voer uw eenmalig wachtwoord in.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Tijdelijk\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Fout\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Voorbeeld:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Overschrijdt {0}{1} in de laatste {2, plural, one {# minuut} other {# minuten}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Uitvoer hoofd-PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Bestaande systemen die niet gedefinieerd zijn in <0>config.yml</0> zullen worden verwijderd. Maak regelmatige backups.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Beëindigd actief\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Verloopt na één uur of bij hub-herstart.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exporteren\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Configuratie exporteren\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exporteer je huidige systeemconfiguratie.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Mislukt\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Mislukte kenmerken:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Authenticatie mislukt\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Instellingen opslaan mislukt\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Verzenden van heartbeat mislukt\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Versturen test notificatie mislukt\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Bijwerken waarschuwing mislukt\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Mislukt: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filteren...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Vingerafdruk\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Voor <0>{min}</0> {min, plural, one {minuut} other {minuten}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Wachtwoord vergeten?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD commando\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Vol\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Algemeen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globaal\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-engines\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU stroomverbruik\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU-gebruik\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Raster\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Gezondheid\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat-monitoring\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat succesvol verzonden\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew-commando\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP-adres\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-methode\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-methode: POST, GET of HEAD (standaard: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inactief\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Als je het wachtwoord voor je beheerdersaccount bent kwijtgeraakt, kan je het opnieuw instellen met behulp van de volgende opdracht.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Afbeelding\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inactief\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Ongeldig e-mailadres.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Taal\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Indeling\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layoutbreedte\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Levenscyclus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limiet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Gemiddelde Belasting\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Gemiddelde Belasting 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Gemiddelde Belasting 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Gemiddelde Belasting 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Gem. Belasting\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Laadstatus\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Laden...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Afmelden\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Aanmelden\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Aanmelding mislukt\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logboeken\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Zoek je waar je meldingen kunt aanmaken? Klik op de bel <0/> in de systeemtabel.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Hoofd-PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Weergave- en notificatievoorkeuren beheren.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Handmatige installatie-instructies\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Geheugen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Geheugenlimiet\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Geheugenpiek\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Geheugengebruik\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Geheugengebruik van docker containers\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Naam\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Netwerk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Netwerkverkeer van docker containers\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Netwerkverkeer van publieke interfaces\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Netwerk eenheid\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nee\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Geen resultaten gevonden.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Geen resultaten.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Geen S.M.A.R.T. kenmerken beschikbaar voor dit apparaat.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Geen systemen gevonden.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Meldingen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC ondersteuning\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Bij elke herstart zullen systemen in de database worden bijgewerkt om overeen te komen met de systemen die in het bestand zijn gedefinieerd.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Eenmalig\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Eenmalig wachtwoord\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Menu openen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Of ga verder met\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Overig\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Overschrijf bestaande waarschuwingen\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Pagina\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Pagina {0} van de {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Pagina's / Instellingen\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Wachtwoord\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Het wachtwoord moet minimaal 8 tekens bevatten.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Het wachtwoord moet minder zijn dat 72 bytes.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Wachtwoord reset aanvraag ontvangen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Verleden\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pauze\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Gepauzeerd\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Gepauzeerd ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Payload-indeling\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Gemiddeld gebruik per kern\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Percentage tijd besteed in elke status\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Blijvend\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistentie\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"<0>Configureer een SMTP-server </0> om ervoor te zorgen dat waarschuwingen worden afgeleverd.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Controleer de logs voor meer details.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Controleer je aanmeldgegevens en probeer het opnieuw\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Maak een beheerdersaccount aan\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Activeer pop-ups voor deze website\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Meld je opnieuw aan\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Bekijk <0>de documentatie</0> voor instructies.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Meld je aan bij je account\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Poort\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Inschakelen\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Nauwkeurig gebruik op de opgenomen tijd\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Voorkeurstaal\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces gestart\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Publieke sleutel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Stille uren\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lezen\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Ontvangen\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Vernieuwen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relaties\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Eenmalig wachtwoord aanvragen\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP aanvragen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Vereist door\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Vereist\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Wachtwoord resetten\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Opgelost\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Herstarten\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Hervatten\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Roteer Token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Rijen per pagina\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Runtime-metrieken\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.-details\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Zelf-test\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Bewaar het adres met de enter-toets of komma. Laat leeg om e-mailmeldingen uit te schakelen.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Instellingen opslaan\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Systeem bewaren\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Opgeslagen in de database en verloopt niet totdat u het uitschakelt.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Schema\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Plan stille uren waarin meldingen niet worden verzonden, zoals tijdens onderhoudsperioden.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Plan stille uren waarin meldingen niet worden verzonden.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Zoeken\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Zoek naar systemen of instellingen...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Seconden tussen pings (standaard: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Zie <0>notificatie-instellingen</0> om te configureren hoe je meldingen ontvangt.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Selecteer {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Stuur een enkele heartbeat-ping om te controleren of je endpoint werkt.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Stuur periodieke uitgaande pings naar een externe monitoringservice, zodat je Beszel kunt monitoren zonder het aan het internet bloot te stellen.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Stuur test-heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Verzonden\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serienummer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Servicedetails\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Stel percentagedrempels in voor meterkleuren.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Stel de volgende omgevingsvariabelen in op je Beszel-hub om heartbeat-monitoring in te schakelen:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Instellingen\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Instellingen opgeslagen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Aanmelden\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP-instellingen\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sorteren op\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Starttijd\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Status\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Substatus\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap ruimte gebruikt door het systeem\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap gebruik\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Systeem\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Gemiddelde systeembelasting na verloop van tijd\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd-services\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systemen\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systemen kunnen worden beheerd in een <0>config.yml</0> bestand in je data map.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabel\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Taken\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatuur\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatuur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperatuureenheid\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperatuur van systeem sensoren\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test-heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testmelding verzonden\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"De algehele status is <0>ok</0> wanneer alle systemen in de lucht zijn, <1>waarschuwing</1> wanneer meldingen zijn geactiveerd, en <2>fout</2> wanneer een systeem plat ligt.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Log vervolgens in op de backend en reset het wachtwoord van je gebruikersaccount in het gebruikersoverzicht.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Deze actie kan niet ongedaan worden gemaakt. Dit zal alle huidige records voor {name} permanent verwijderen uit de database.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Dit zal alle geselecteerde records verwijderen uit de database.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Doorvoer van {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Doorvoer van het root bestandssysteem\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Tijdnotatie\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Naar e-mail(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Schakel raster\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Schakel thema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens & Vingerafdrukken\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens staan agenten toe om verbinding te maken met en te registreren. Vingerafdrukken zijn stabiele Ids deze zijn uniek voor elk systeem, ingesteld bij eerste verbinding.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens en vingerafdrukken worden gebruikt om WebSocket verbindingen te verifiëren naar de hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Totaal\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Totaal ontvangen gegevens per interface\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Totaal verzonden gegevens per interface\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Totaal: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Geactiveerd door\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Triggers\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Triggert wanneer de gemiddelde belasting een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Triggert wanneer de 15 minuten gemiddelde belasting een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Triggert wanneer de 5 minuten gemiddelde belasting een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Triggert wanneer een sensor een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Triggert wanneer de batterijlading onder een drempelwaarde daalt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Triggert wanneer de gecombineerde up/down een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Triggert wanneer het CPU-gebruik een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Triggert wanneer het GPU-gebruik een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Triggert wanneer het geheugengebruik een drempelwaarde overschrijdt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Triggert wanneer de status schakelt tussen up en down\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Triggert wanneer het gebruik van een schijf een drempelwaarde overschrijdt\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Type\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Unit-bestand\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Eenheid voorkeuren\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universele token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Onbekend\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Onbeperkt\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Online\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Online ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Bijwerken\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Bijgewerkt\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Elke 10 minuten bijgewerkt.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Uploaden\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Actief\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Gebruik\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Gebruik van root-partitie\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Gebruikt\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Gebruikers\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Waarde\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Weergave\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Meer weergeven\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Bekijk je 200 meest recente meldingen.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Zichtbare kolommen\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Wachtend op genoeg records om weer te geven\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Wil je ons helpen onze vertalingen nog beter te maken? Bekijk <0>Crowdin</0> voor meer informatie.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Wil\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Waarschuwing (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Waarschuwingsdrempels\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Pushmeldingen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Indien ingeschakeld, stelt deze token agenten in staat zich zelf te registreren zonder voorafgaande systeemcreatie.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Bij gebruik van POST bevat elke heartbeat een JSON-payload met een samenvatting van de systeemstatus, een lijst met uitgevallen systemen en geactiveerde meldingen.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows-commando\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Schrijven\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML Configuratie\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Configuratie\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Je gebruikersinstellingen zijn bijgewerkt.\"\n"
  },
  {
    "path": "internal/site/src/locales/no/no.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: no\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Norwegian\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: no\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} av {1} rad(er) valgt.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# kjerne} other {# kjerner}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dag} other {{countString} dager}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} time} other {{countString} timer}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minutt} other {{countString} minutter}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# tråd} other {# tråder}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 time\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minutt\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 uke\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 timer\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 timer\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dager\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Handlinger\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktiv\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktive Alarmer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktiv tilstand\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Legg til {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Legg til <0>System</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Legg til system\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Legg Til URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Juster visningsalternativer for diagrammer.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Juster bredden på hovedlayouten\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Etter\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Etter å ha angitt miljøvariablene, start Beszel-huben på nytt for at endringene skal tre i kraft.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Varselhistorikk\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alarmer\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Alle containere\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Alle Systemer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Er du sikker på at du vil slette {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Er du sikker?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatisk kopiering krever en sikker kontekst.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Gjennomsnitt\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Gjennomsnittlig CPU-utnyttelse av konteinere\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Gjennomsnittet faller under <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Gjennomsnittet overstiger <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Gjennomsnittlig strømforbruk for GPU-er\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Gjennomsnittlig CPU-utnyttelse for hele systemet\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Gjennomsnittlig utnyttelse av {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Gjennomsnittlig utnyttelse av GPU-motorer\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Sikkerhetskopier\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Båndbredde\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Batteri\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batteri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Ble aktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Ble inaktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Før\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Under {0}{1} i siste {2, plural, one {# minutt} other {# minutter}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel støtter OpenID Connect og mange OAuth2 autentiserings-tilbydere.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel bruker <0>Shoutrrr</0> for integrering mot populære meldingstjenester.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binær\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Oppstartstilstand\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffere\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Kan laste inn på nytt\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Kan starte\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Kan stoppe\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Avbryt\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Kapabiliteter\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapasitet\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Advarsel - potensielt tap av data\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Endre måleenheter for målinger.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Endre generelle program-innstillinger.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Lading\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Lader\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Diagraminnstillinger\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Sjekk {email} for en nullstillings-link.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Sjekk loggene for flere detaljer.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Sjekk overvåkingstjenesten din\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Sjekk din meldingstjeneste\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Tøm\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klikk på en container for å se mer informasjon.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klikk på en enhet for å se mer informasjon.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klikk på et system for å se mer informasjon.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klikk for å kopiere\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Kommandolinje-instrukser\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurer hvordan du vil motta alarmvarsler.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Bekreft passord\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikter\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Tilkoblingen er nede\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Fortsett\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Kopiert til utklippstavlen\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopier docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Kopier docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopier env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopier vert\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopier Linux-kommando\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopier navn\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopier tekst\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopier installasjonskommandoen for agenten nedenfor, eller registrer agenter automatisk med en <0>universal token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopier <0>docker-compose.yml</0> for agenten nedenfor, eller registrer agenter automatisk med en <0>universal token</0>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopier YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU-kjerner\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU-topp\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU-tid\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU-tidsoppdeling\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU-bruk\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Opprett\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Opprett konto\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Opprettet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritisk (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativ nedlasting\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativ opplasting\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Nåværende tilstand\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Sykluser\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Daglig\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standard tidsperiode\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Slett\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Slett fingeravtrykk\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Beskrivelse\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detaljer\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Enhet\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Lader ut\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Diskenhet\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Diskbruk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Diskbruk av {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU-bruk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker Minnebruk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Nettverks-I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentasjon\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nede\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nede ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Last ned\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Varighet\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Rediger\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Rediger {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-post\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-postvarslinger\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Tom\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Sluttid\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Endepunkt-URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Endepunkt-URL som skal pinges (påkrevd)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Skriv inn e-postadresse for å nullstille passordet\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Skriv inn e-postadresse...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Skriv inn ditt engangspassord.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Flyktig\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Feil\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Eksempel:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Overstiger {0}{1} {2, plural, one {det siste minuttet} other {de siste # minuttene}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Hovedprosess-ID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Eksisterende systemer som ikke er er definert i <0>config.yml</0> vil bli slettet. Vennligst ta jevnlige sikkerhetskopier.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Avsluttet aktiv\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Utløper etter en time eller ved hub-omstart.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Eksporter\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Eksporter konfigurasjon\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Eksporter din nåværende systemkonfigurasjon\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Mislyktes\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Mislykkede attributter:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Autentisering mislyktes\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Kunne ikke lagre innstillingene\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Kunne ikke sende heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Kunne ikke sende test-varsling\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Kunne ikke oppdatere alarm\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Mislyktes: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filter...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingeravtrykk\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Fastvare\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"I <0>{min}</0> {min, plural, one {minutt} other {minutter}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Glemt passord?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD kommando\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Fullt\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Generelt\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-motorer\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU Effektforbruk\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU-bruk\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Rutenett\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Helse\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat-overvåking\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat sendt\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew-kommando\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Vert / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-metode\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-metode: POST, GET eller HEAD (standard: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inaktiv\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Dersom du har mistet passordet til admin-kontoen kan du nullstille det med følgende kommando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Image\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inaktiv\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervall\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Ugyldig e-postadresse.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Språk\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Oppsett\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layoutbredde\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Livssyklus\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"grense\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Snittbelastning Last\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Snittbelastning 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Snittbelastning 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Snittbelastning 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Snittbelastning\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Lastetilstand\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Laster...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Logg Ut\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Logg Inn\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Innlogging mislyktes\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logger\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Ser du etter hvor du kan opprette alarmer? Klikk på bjelle-ikonet <0/> for systemet i systemoversikten.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Hovedprosess-ID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Endre visnings- og varslingsinnstillinger.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Instruks for Manuell Installasjon\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maks 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Minne\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Minnegrense\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Minne-topp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Minnebruk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Minnebruk av docker-konteinere\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modell\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Navn\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Nett\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Nettverkstrafikk av docker-konteinere\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Nettverkstrafikk av eksterne nettverksgrensesnitt\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Nettverksenhet\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nei\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Ingen resultater funnet.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Ingen resultater.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Ingen S.M.A.R.T.-attributter tilgjengelig for denne enheten.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Ingen systemer funnet.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Varslinger\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC-støtte\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Ved hver omstart vil systemer i databasen bli oppdatert til å matche systemene definert i fila.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Engangs\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Engangspassord\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Åpne meny\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Eller fortsett med\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Andre\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Overskriv eksisterende alarmer\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Side\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Side {0} av {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Sider / Innstillinger\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Passord\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Passord må bestå av minst 8 tegn.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Passord må være mindre enn 72 byte.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Mottatt forespørsel om å nullstille passord\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Fortid\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pause\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Satt på Pause\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pauset ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Nyttelastformat\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Gjennomsnittlig utnyttelse per kjerne\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Prosentandel av tid brukt i hver tilstand\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Vedvarenhet\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Vennligst <0>konfigurer en SMTP-server</0> for å forsikre deg om at varsler blir levert.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Vennligst sjekk loggene for mer informasjon.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Vennligst kontroller dine innloggingsopplysninger og prøv igjen\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Vennligst opprett en admin-konto\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Vennligst aktiver pop-ups for nettsiden\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Vennligst logg inn på nytt\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Vennligst se <0>dokumentasjonen</0> for instrukser.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Vennligst logg inn på kontoen din\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Påslag\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Nøyaktig utnyttelse på registrert tidspunkt\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Foretrukket Språk\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Prosess startet\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Offentlig Nøkkel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Stille timer\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Lesing\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Mottatt\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Oppdater\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relasjoner\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Be om engangspassord\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Be om OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Påkrevd av\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Påkrevd\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Nullstill Passord\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Løst\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Omstarter\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Gjenoppta\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Rot\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Forny token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Rader per side\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Kjøretidsmålinger\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.-detaljer\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. selvtest\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Lagre adressen med Enter-tasten eller komma. La feltet være tomt for å deaktivere e-postvarsler.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Lagre Innstillinger\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Lagre system\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Lagret i databasen og utløper ikke før du deaktiverer det.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Tidsplan\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Planlegg stille timer hvor varsler ikke sendes, for eksempel under vedlikeholdsperioder.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Planlegg stille timer hvor varsler ikke sendes.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Søk\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Søk etter systemer eller innstillinger...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunder mellom pinger (standard: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Se <0>varslingsinnstillingene</0> for å konfigurere hvordan du vil motta varsler.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Velg {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Send en enkelt heartbeat-ping for å bekrefte at endepunktet fungerer.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Send periodiske utgående pinger til en ekstern overvåkingstjeneste slik at du kan overvåke Beszel uten å eksponere den for internett.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Send test-heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Sendt\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serienummer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Tjenestedetaljer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Tjenester\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Angi prosentvise terskler for målerfarger.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Angi følgende miljøvariabler på Beszel-huben din for å aktivere heartbeat-overvåking:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Innstillinger\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Innstillinger lagret\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Logg inn\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP-innstillinger\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sorter Etter\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Starttid\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Tilstand\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Undertilstand\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap-plass i bruk av systemet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap-bruk\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Systembelastning gjennomsnitt over tid\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd-tjenester\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systemer\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systemer kan håndteres i en <0>config.yml</0>-fil i din data-katalog.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabell\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Oppgaver\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperaturenhet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturer på system-sensorer\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test-heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Test-varsling sendt\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Den generelle statusen er <0>ok</0> når alle systemer er oppe, <1>varsel</1> når varsler utløses, og <2>feil</2> når et system er nede.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Logg deretter inn i backend og nullstill passordet på din konto i users-tabellen.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Denne handlingen kan ikke omgjøres. Dette vil slette alle poster for {name} permanent fra databasen.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Dette vil permanent slette alle valgte oppføringer fra databasen.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Gjennomstrømning av {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Gjennomstrømning av rot-filsystemet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Tidsformat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Til e-postadresse(r)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Rutenett av/på\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Tema av/på\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens & Fingeravtrykk\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens lar agenter koble til og registrere seg selv. Fingeravtrykk er stabile identifikatorer som er unike for hvert system, og blir satt ved første tilkobling.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens og fingeravtrykk blir brukt for å autentisere WebSocket-tilkoblinger til huben.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Totalt\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Totalt mottatt data for hvert grensesnitt\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Totalt sendt data for hvert grensesnitt\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Totalt: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Utløst av\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Utløsere\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Slår inn når gjennomsnittsbelastningen over 1 minutt overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Slår inn når gjennomsnittsbelastningen over 15 minutter overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Slår inn når gjennomsnittsbelastningen over 5 minutter overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Slår inn når hvilken som helst sensor overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Utløses når batterilading faller under en terskel\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Slår inn når kombinert opp/ned overskrider en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Slår inn når CPU-bruken overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Slår inn når GPU-bruken overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Slår inn når minnebruken overstiger en grenseverdi\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Slår inn når statusen veksler mellom oppe og nede\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Slår inn når forbruk av hvilken som helst disk overstiger en grenseverdi\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Type\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Enhetsfil\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Enhetspreferanser\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universal token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Ukjent\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Ubegrenset\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Oppe\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Oppe ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Oppdater\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Oppdatert\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Oppdatert hvert 10. minutt.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Last opp\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Oppetid\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Forbruk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Forbruk av rot-partisjon\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Brukt\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Brukere\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Verdi\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Visning\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Se mer\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Vis de 200 siste varslene.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Synlige Felter\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Venter på nok registreringer til å vise\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Vil du hjelpe oss med å gjøre oversettelsene enda bedre? Ta en titt på <0>Crowdin</0> for mer informasjon.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Ønsker\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Advarsel (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Advarselsterskler\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push-varslinger\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Når aktivert, tillater denne tokenen agenter å registrere seg selv uten forutgående systemskapelse.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Ved bruk av POST inkluderer hver heartbeat en JSON-nyttelast med systemstatussammendrag, liste over nede systemer og utløste varsler.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows-kommando\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Skriving\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML Oppsett\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Konfigurasjon\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Dine brukerinnstillinger har blitt oppdatert.\"\n"
  },
  {
    "path": "internal/site/src/locales/pl/pl.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pl\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Polish\\n\"\n\"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: pl\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} z {1} wybranych wierszy.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# rdzeń} few {# rdzenie} many {# rdzeni} other {# rdzeni}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dzień} few {{countString} dni} many {{countString} dni} other {{countString} dni}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {godzinę} few {{countString} godziny} many {{countString} godzin} other {{countString} godziny}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuta} few {{countString} minuty} many {{countString} minut} other {{countString} minut}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# wątek} few {# wątki} many {# wątków} other {# wątków}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 godzina\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuta\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 tydzień\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 godzin\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 godziny\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dni\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Akcje\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktywny\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktywne alerty\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Stan aktywny\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Dodaj {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Dodaj <0>system</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Dodaj system\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Dodaj URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Dostosuj opcje wyświetlania wykresów.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Dostosuj szerokość widoku\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Po\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Po ustawieniu zmiennych środowiskowych zrestartuj hub Beszel, aby zmiany weszły w życie.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Historia alertów\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alerty\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Wszystkie kontenery\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Wszystkie systemy\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Czy na pewno chcesz usunąć {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Czy jesteś pewien?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatyczne kopiowanie wymaga bezpiecznego kontekstu.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Średnia\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Średnie wykorzystanie CPU przez kontenery\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Średnia spada poniżej <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Średnia przekracza <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Średnie zużycie energii przez GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Średnie wykorzystanie CPU w całym systemie\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Średnie użycie {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Średnie wykorzystanie silników GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Kopie zapasowe\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Przepustowość\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bateria\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Bateria\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Stało się aktywnym\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Stało się nieaktywnym\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Przed\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Poniżej {0}{1} w ciągu ostatnich {2, plural, one {# minuty} other {# minut}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel obsługuje OpenID Connect i wielu dostawców uwierzytelniania OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel używa <0>Shoutrrr</0> do integracji z popularnymi serwisami powiadomień.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Plik binarny\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bity (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Stan rozruchu\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bajty (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Pamięć podręczna / Bufory\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Może przeładować\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Może uruchomić\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Może zatrzymać\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Anuluj\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Możliwości\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Pojemność\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Uwaga - ryzyko utraty danych\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsjusza (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Zmień jednostki wyświetlania dla metryk.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Zmień ogólne ustawienia aplikacji.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Ładowanie\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Ładuje się\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Wykresy\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Sprawdź {email}, aby uzyskać link do resetowania.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Sprawdź logi, aby uzyskać więcej informacji.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Sprawdź usługę monitorowania\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Sprawdź swój serwis powiadomień\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Wyczyść\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Wybierz kontener, aby wyświetlić więcej informacji.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Wybierz urządzenie, aby wyświetlić więcej informacji.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Wybierz system, aby wyświetlić więcej informacji.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Kliknij, aby skopiować\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instrukcje wiersza poleceń\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Skonfiguruj sposób otrzymywania powiadomień.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Potwierdź hasło\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikty\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Brak połączenia\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Kontynuuj\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Skopiowano do schowka\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Skopiuj docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Skopiuj docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopiuj env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopiuj host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopiuj polecenie Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopiuj nazwę\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopiuj tekst\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Skopiuj poniżej polecenie instalacji agenta albo zarejestruj agentów automatycznie za pomocą <0>uniwersalnego tokenu</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Skopiuj poniżej zawartość pliku <0>docker-compose.yml</0> dla agenta lub zarejestruj agentów automatycznie przy użyciu <1>uniwersalnego tokenu</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopiuj YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"Procesor\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Rdzenie CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Szczyt CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Czas CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Podział czasu CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Użycie procesora\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Utwórz\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Utwórz konto\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Utworzono\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Krytyczny (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Pobieranie łącznie\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Wysyłanie łącznie\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Aktualny stan\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cykle\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Codziennie\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Domyślny przedział czasu\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Usuń\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Usuń odcisk palca\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Opis\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Szczegół\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Urządzenie\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Rozładowuje się\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Dysk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Dysk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Jednostka dysku\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Użycie dysku\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Wykorzystanie dysku {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Użycie CPU przez Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Użycie pamięci przez Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Sieć Docker I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentacja\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nie działa\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nie działa ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Pobieranie\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Czas trwania\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Edytuj\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Edytuj {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-mail\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Powiadomienia e-mail\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Pusta\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Czas zakończenia\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Adres URL punktu końcowego\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Adres URL punktu końcowego do pingowania (wymagany)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Wprowadź adres e-mail, aby zresetować hasło\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Wprowadź adres e-mail...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Wprowadź swoje jednorazowe hasło.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Tymczasowy\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Błąd\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Przykład:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Przekracza {0}{1} w ciągu ostatnich {2, plural, one {# minuty} other {# minut}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Główny PID wykonania\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Istniejące systemy, które nie są zdefiniowane w <0>config.yml</0>, zostaną usunięte. Pamiętaj aby regularnie tworzyć kopie zapasowe.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Zakończono aktywnie\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Wygasa po godzinie lub przy ponownym uruchomieniu huba.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Eksport\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Eksportuj konfigurację\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Eksportuj aktualną konfigurację systemów.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Nieudane\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Nieudane atrybuty:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Błąd autoryzacji\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Nie udało się zapisać ustawień\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Nie udało się wysłać heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Nie udało się wysłać powiadomienia testowego\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Nie udało się zaktualizować powiadomienia\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Nieudane: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtruj...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Odcisk palca\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Oprogramowanie sprzętowe\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Na <0>{min}</0> {min, plural, one {minutę} other {minut}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Zapomniałeś hasła?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Polecenie FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Pełna\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Ogólne\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globalny\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Silniki GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Moc GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Użycie GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Siatka\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Kondycja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitorowanie Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat wysłany pomyślnie\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Polecenie Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / adres IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Metoda HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Metoda HTTP: POST, GET lub HEAD (domyślnie: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Bezczynna\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Jeśli utraciłeś hasło do swojego konta administratora, możesz je zresetować, używając następującego polecenia.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Obraz\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Nieaktywny\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interwał\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Nieprawidłowy adres e-mail.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Język\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Układ\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Szerokość układu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Cykl życia\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limit\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Średnie obciążenie\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Średnie obciążenie 15 min\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Średnie obciążenie 1 min\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Średnie obciążenie 5 min\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Śr. obciążenie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Stan obciążenia\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Ładowanie...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Wyloguj\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Logowanie\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Próba logowania nie powiodła się\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logi\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Szukasz, gdzie utworzyć powiadomienia? Kliknij ikonę dzwonka <0/> w tabeli systemów.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Główny PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Zarządzaj preferencjami wyświetlania i powiadomień.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Instrukcja ręcznej konfiguracji\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maks. 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Pamięć\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Limit pamięci\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Szczyt pamięci\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Użycie pamięci\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Użycie pamięci przez kontenery Docker.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nazwa\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Sieć\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Ruch sieciowy kontenerów Docker.\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Ruch sieciowy interfejsów publicznych\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Jednostka sieciowa\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nie\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nie znaleziono wyników.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Brak wyników.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Brak dostępnych atrybutów S.M.A.R.T. dla tego urządzenia.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nie znaleziono systemów.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Powiadomienia\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Wsparcie OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Przy każdym ponownym uruchomieniu systemy w bazie danych będą aktualizowane, aby odpowiadały systemom zdefiniowanym w pliku.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Jednorazowy\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Hasło jednorazowe\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Otwórz menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Lub kontynuuj z\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Inne\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Nadpisz istniejące alerty\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Strona\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Strona {0} z {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Strony / Ustawienia\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Hasło\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Hasło musi zawierać co najmniej 8 znaków.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Hasło musi być mniejsze niż 72 bajty.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Otrzymano żądanie resetowania hasła\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Poprzednie\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pauza\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Wstrzymane\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Wstrzymane ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Format ładunku\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Średnie wykorzystanie na rdzeń\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Procent czasu spędzonego w każdym stanie\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Stały\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Trwałość\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Proszę <0>skonfigurować serwer SMTP</0>, aby zapewnić dostarczanie powiadomień.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Sprawdź logi, aby uzyskać więcej informacji.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Sprawdź swoje poświadczenia i spróbuj ponownie\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Utwórz konto administratora\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Włącz wyskakujące okna dla tej strony\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Zaloguj się ponownie\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Proszę zapoznać się z <0>dokumentacją</0>.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Zaloguj się na swoje konto\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Włączony\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Dokładne wykorzystanie w zarejestrowanym czasie\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Preferowany język\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces uruchomiony\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Klucz publiczny\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Godziny ciszy\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Odczyt\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Otrzymane\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Odśwież\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relacje\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Zażądaj jednorazowego hasła\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Zażądaj OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Wymagane przez\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Wymaga\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Resetuj hasło\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Rozwiązany\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Uruchamia ponownie\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Wznów\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Zmień token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Wiersze na stronę\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metryki czasu wykonania\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Szczegóły S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Samodiagnostyka S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Zapisz adres, używając klawisza enter lub przecinka. Pozostaw puste, aby wyłączyć powiadomienia e-mail.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Zapisz ustawienia\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Zapisz system\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Zapisany w bazie danych. Nie wygasa, dopóki go nie wyłączysz.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Harmonogram\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Zaplanuj godziny ciszy, w których powiadomienia nie będą wysyłane, na przykład podczas okresów konserwacji.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Zaplanuj godziny ciszy, w których powiadomienia nie będą wysyłane.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Szukaj\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Szukaj systemów lub ustawień...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekundy między pingami (domyślnie: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Zobacz <0>ustawienia powiadomień</0>, aby skonfigurować sposób, w jaki otrzymujesz powiadomienia.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Wybierz {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Wyślij pojedynczy ping heartbeat, aby sprawdzić, czy punkt końcowy działa.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Wysyłaj okresowe pingi wychodzące do zewnętrznej usługi monitorowania, dzięki czemu możesz monitorować Beszel bez wystawiania go na działanie Internetu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Wyślij testowy heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Wysłane\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Numer seryjny\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Szczegóły usługi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Usługi\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Ustaw progi procentowe dla kolorów mierników.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Ustaw następujące zmienne środowiskowe w hubie Beszel, aby włączyć monitorowanie heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Ustawienia\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Ustawienia zostały zapisane\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Zaloguj się\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Ustawienia SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sortuj według\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Czas rozpoczęcia\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Stan\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Stan podrzędny\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Pamięć wymiany używana przez system\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Użycie pamięci wymiany\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Średnie obciążenie systemu w czasie\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Usługi systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Systemy\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Systemy mogą być zarządzane w pliku <0>config.yml</0> znajdującym się w Twoim katalogu danych.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabela\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Zadania\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Jednostka temperatury\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperatury czujników systemowych.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Test <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testuj heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testowe powiadomienie wysłane.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Ogólny status to <0>ok</0>, gdy wszystkie systemy działają, <1>ostrzeżenie</1>, gdy wyzwalane są alerty, oraz <2>błąd</2>, gdy którykolwiek system nie działa.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Następnie zaloguj się do panelu administracyjnego i  zresetuj hasło do konta użytkownika w tabeli użytkowników.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Tej akcji nie można cofnąć. Spowoduje to trwałe usunięcie wszystkich bieżących rekordów dla {name} z bazy danych.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Spowoduje to trwałe usunięcie wszystkich zaznaczonych rekordów z bazy danych.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Przepustowość {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Przepustowość głównego systemu plików\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Format czasu\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Do e-mail(ów)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Przełącz widok\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Zmień motyw\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokeny & odciski palców\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokeny umożliwiają agentom łączenie się i rejestrację. Odciski palców (fingerprinty) to stałe, unikalne identyfikatory przypisane do każdego systemu przy pierwszym połączeniu.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokeny i odciski palców (fingerprinty) służą do uwierzytelniania połączeń WebSocket z hubem.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Łącznie\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Całkowita ilość danych odebranych dla każdego interfejsu\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Całkowita ilość danych wysłanych dla każdego interfejsu\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Łącznie: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Wyzwalane przez\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Wyzwalacze\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Uruchamia się, gdy 1-minutowe średnie obciążenie systemu przekroczy ustawiony próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Uruchamia się, gdy 15-minutowe średnie obciążenie systemu przekroczy ustawiony próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Uruchamia się, gdy 5-minutowe średnie obciążenie systemu przekroczy ustawiony próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg.\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Uruchamia się, gdy poziom baterii spadnie poniżej wybranej wartości\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Wyzwalane, gdy łączna wartość w górę/w dół przekroczy próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Wyzwalane, gdy użycie procesora przekracza próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Wyzwalane, gdy użycie GPU przekroczy próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Wyzwalane, wykorzystanie pamięci przekroczy ustalony próg\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Wyzwalane, gdy status przełącza się między stanem aktywnym a nieaktywnym\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony próg\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Typ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Plik jednostki\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Ustawienia jednostek\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Uniwersalny token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Nieznana\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Bez limitu\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Działa\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Działa ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Aktualizuj\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Zaktualizowano\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Aktualizowane co 10 minut.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Wysyłanie\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Wykorzystanie\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Użycie partycji głównej\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Używane\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Użytkownicy\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Wartość\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Widok\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Zobacz więcej\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Wyświetl 200 ostatnich alertów.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Widoczne kolumny\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Oczekiwanie na wystarczającą liczbę rekordów do wyświetlenia\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Chcesz pomóc ulepszyć nasze tłumaczenie? Sprawdź <0>Crowdin</0> po szczegóły.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Wymaga\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Ostrzeżenie (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Progi ostrzegawcze\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Powiadomienia push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Gdy jest włączony, ten token pozwala agentom na samodzielną rejestrację bez wcześniejszego tworzenia systemu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"W przypadku korzystania z POST każdy heartbeat zawiera ładunek JSON z podsumowaniem statusu systemu, listą wyłączonych systemów i wyzwolonymi alertami.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Polecenie Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Zapis\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Konf. YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Konfiguracja YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Tak\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Twoje ustawienia użytkownika zostały zaktualizowane.\"\n"
  },
  {
    "path": "internal/site/src/locales/pt/pt.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: pt\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Portuguese\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: pt-PT\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} de {1} linha(s) selecionada(s).\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# núcleo} other {# núcleos}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dia} other {{countString} dias}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} hora} other {{countString} horas}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuto} other {{countString} minutos}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# thread} other {# threads}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 hora\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuto\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 semana\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 horas\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 horas\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dias\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Ações\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Ativo\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Alertas Ativos\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Estado ativo\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Adicionar {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Adicionar <0>Sistema</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Adicionar sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Adicionar URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Ajustar opções de exibição para gráficos.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Ajustar a largura do layout principal\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Depois\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Após configurar as variáveis de ambiente, reinicie o hub do Beszel para que as alterações entrem em vigor.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agente\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Histórico de alertas\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Alertas\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Todos os Contêineres\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Todos os Sistemas\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Tem certeza de que deseja excluir {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Tem certeza?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"A cópia automática requer um contexto seguro.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Média\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Utilização média de CPU dos contêineres\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"A média cai abaixo de <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"A média excede <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Consumo médio de energia pelas GPU's\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Utilização média de CPU em todo o sistema\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Utilização média de {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Utilização média dos motores GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Cópias de segurança\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Largura de Banda\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Bateria\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Tornou-se ativo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Tornou-se inativo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Antes\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Abaixo de {0}{1} no último {2, plural, one {# minuto} other {# minutos}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel suporta OpenID Connect e muitos provedores de autenticação OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel usa <0>Shoutrrr</0> para integrar com serviços de notificação populares.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binário\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bits (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Estado de inicialização\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffers\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Pode recarregar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Pode iniciar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Pode parar\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Capacidades\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Capacidade\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Cuidado - possível perda de dados\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Alterar unidades de exibição para métricas.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Alterar opções gerais do aplicativo.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Carga\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Carregando\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Opções de gráfico\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Verifique {email} para um link de redefinição.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Verifique os logs para mais detalhes.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Verifique o seu serviço de monitorização\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Verifique seu serviço de notificação\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Limpar\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Clique num contentor para ver mais informações.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Clique em um dispositivo para ver mais informações.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Clique em um sistema para ver mais informações.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Clique para copiar\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instruções de linha de comando\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Configure como você recebe notificações de alerta.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Confirmar senha\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Conflitos\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"A conexão está inativa\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Continuar\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Copiado para a área de transferência\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Copiar docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Copiar docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Copiar variáveis de ambiente\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Copiar host\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Copiar comando Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Copiar nome\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Copiar texto\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Copie o comando de instalação do agente abaixo, ou registre agentes automaticamente com um <0>token universal</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Copie o conteúdo do <0>docker-compose.yml</0> do agente abaixo, ou registre agentes automaticamente com um <1>token universal</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Copiar YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Núcleos de CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Pico de CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Tempo de CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Distribuição do Tempo de CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Uso de CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Criar\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Criar conta\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Criado\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Crítico (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Download cumulativo\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Upload cumulativo\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Estado atual\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Ciclos\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Diariamente\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Período de tempo padrão\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Excluir\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Excluir impressão digital\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Descrição\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detalhe\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Dispositivo\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Descarregando\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"E/S de Disco\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Unidade de disco\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Uso de Disco\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Uso de disco de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Uso de CPU do Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Uso de Memória do Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"E/S de Rede do Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Documentação\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Desligado\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Inativo ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Transferir\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Duração\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Editar\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Editar {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Notificações por email\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Vazia\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Hora de Fim\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL do Endpoint\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL do Endpoint para ping (obrigatório)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Digite o endereço de email para redefinir a senha\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Digite o endereço de email...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Insira a sua senha de uso único.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Efêmero\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Erro\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Exemplo:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Excede {0}{1} no último {2, plural, one {# minuto} other {# minutos}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID principal de execução\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Sistemas existentes não definidos em <0>config.yml</0> serão excluídos. Faça backups regulares.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Saiu ativo\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Expira após uma hora ou no reinício do hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportar\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Exportar configuração\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exporte a configuração atual dos seus sistemas.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Falhou\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Atributos com Falha:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Falha na autenticação\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Falha ao guardar as definições\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Falha ao enviar o batimento cardíaco\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Falha ao enviar notificação de teste\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Falha ao atualizar alerta\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Falhou: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtrar...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Impressão digital\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Firmware\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Por <0>{min}</0> {min, plural, one {minuto} other {minutos}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Esqueceu a senha?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Comando FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Cheia\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Geral\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Motores GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Consumo de Energia da GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Uso de GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Grade\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Saúde\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitorização de Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat enviado com sucesso\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Comando Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Método HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Método HTTP: POST, GET ou HEAD (predefinido: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Inativa\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Se você perdeu a senha da sua conta de administrador, pode redefini-la usando o seguinte comando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Imagem\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inativo\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervalo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Endereço de email inválido.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Idioma\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Aspeto\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Largura do layout\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Ciclo de vida\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"limite\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Carga Média\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Carga média 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Carga média 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Carga média 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Carga Média\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Estado de carga\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Carregando...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Sair\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Entrar\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Tentativa de login falhou\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Logs\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Procurando onde criar alertas? Clique nos ícones de sino <0/> na tabela de sistemas.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID principal\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Gerenciar preferências de exibição e notificação.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Instruções de configuração manual\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Máx 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Memória\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Limite de memória\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Pico de memória\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Uso de Memória\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Uso de memória dos contêineres Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modelo\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Nome\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Rede\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Tráfego de rede dos contêineres Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Tráfego de rede das interfaces públicas\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Unidade de rede\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Não\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Nenhum resultado encontrado.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Sem resultados.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Nenhum atributo S.M.A.R.T. disponível para este dispositivo.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Nenhum sistema encontrado.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Notificações\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Suporte a OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"A cada reinício, os sistemas no banco de dados serão atualizados para corresponder aos sistemas definidos no arquivo.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Uma vez\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Senha de uso único\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Abrir menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Ou continue com\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Outro\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Sobrescrever alertas existentes\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Página\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Página {0} de {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Páginas / Configurações\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Senha\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"A senha deve ter pelo menos 8 caracteres.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"A password tem que ter menos de 72 bytes.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Solicitação de redefinição de senha recebida\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Passado\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Pausar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pausado\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pausado ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Formato do payload\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Utilização média por núcleo\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Percentagem de tempo gasto em cada estado\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanente\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Persistência\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Por favor, <0>configure um servidor SMTP</0> para garantir que os alertas sejam entregues.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Por favor, verifique os logs para mais detalhes.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Por favor, verifique suas credenciais e tente novamente\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Por favor, crie uma conta de administrador\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Por favor, habilite pop-ups para este site\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Por favor, faça login novamente\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Por favor, veja <0>a documentação</0> para instruções.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Por favor, entre na sua conta\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Porta\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Ligado\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Utilização precisa no momento registrado\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Idioma Preferido\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Processo iniciado\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Chave Pública\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Horas Silenciosas\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Ler\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Recebido\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Atualizar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relacionamentos\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Solicitar senha de uso único\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Solicitar OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Requerido por\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Requer\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Redefinir Senha\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Resolvido\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Reinícios\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Retomar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Rotacionar token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Linhas por página\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Métricas de tempo de execução\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Detalhes S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Auto-teste S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Salve o endereço usando a tecla enter ou vírgula. Deixe em branco para desativar notificações por email.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Guardar Definições\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Guardar Sistema\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Salvo no banco de dados e não expira até você desativá-lo.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Agendar\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Agende horas silenciosas onde as notificações não serão enviadas, como durante períodos de manutenção.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Agende horas silenciosas onde as notificações não serão enviadas.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Pesquisar\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Pesquisar por sistemas ou configurações...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Segundos entre pings (predefinido: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Veja <0>configurações de notificação</0> para configurar como você recebe alertas.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Selecionar {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Envie um único ping de heartbeat para verificar se o seu endpoint está a funcionar.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Envie pings de saída periódicos para um serviço de monitorização externo para que possa monitorizar o Beszel sem o expor à internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Enviar heartbeat de teste\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Enviado\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Número de Série\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Detalhes do serviço\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Serviços\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Defina os limiares de porcentagem para as cores do medidor.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Defina as seguintes variáveis de ambiente no seu hub do Beszel para ativar a monitorização de heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Configurações\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Definições guardadas\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Entrar\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Configurações SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Ordenar Por\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Hora de Início\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Estado\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Estado\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Subestado\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Espaço de swap usado pelo sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Uso de Swap\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistema\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Médias de carga do sistema ao longo do tempo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Serviços Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistemas\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Os sistemas podem ser gerenciados em um arquivo <0>config.yml</0> dentro do seu diretório de dados.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabela\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tarefas\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Unidade de temperatura\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturas dos sensores do sistema\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Testar <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testar heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Notificação de teste enviada\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"O estado geral é <0>ok</0> quando todos os sistemas estão ativos, <1>aviso</1> quando os alertas são acionados e <2>erro</2> quando qualquer sistema está inativo.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Em seguida, faça login no backend e redefina a senha da sua conta de usuário na tabela de usuários.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Esta ação não pode ser desfeita. Isso excluirá permanentemente todos os registros atuais de {name} do banco de dados.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Isso excluirá permanentemente todos os registros selecionados do banco de dados.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Taxa de transferência de {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Taxa de transferência do sistema de arquivos raiz\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Formato de hora\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Para email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Alternar grade\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Alternar tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens e Impressões Digitais\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Os tokens permitem que os agentes se conectem e registrem. As impressões digitais são identificadores estáveis únicos para cada sistema, definidos na primeira conexão.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens e impressões digitais são usados para autenticar conexões WebSocket ao hub.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Dados totais recebidos para cada interface\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Dados totais enviados para cada interface\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Total: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Acionado por\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Acionadores\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Dispara quando a média de carga de 1 minuto excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Dispara quando a média de carga de 15 minutos excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Dispara quando a média de carga de 5 minutos excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Dispara quando qualquer sensor excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Dispara quando a carga da bateria cai abaixo de um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Dispara quando a soma de subida/descida excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Dispara quando o uso de CPU excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Dispara quando o uso de GPU excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Dispara quando o uso de memória excede um limite\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Dispara quando o status alterna entre ativo e inativo\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Dispara quando o uso de qualquer disco excede um limite\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Tipo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Arquivo de unidade\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Preferências de unidade\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token universal\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Desconhecida\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Ilimitado\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Ligado\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Ativo ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Atualizar\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Atualizado\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Atualizado a cada 10 minutos.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Carregar\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Uso\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Uso da partição raiz\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Usado\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Usuários\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Valor\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Visual\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Ver mais\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Veja os seus 200 alertas mais recentes.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Campos Visíveis\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Aguardando registros suficientes para exibir\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Quer nos ajudar a melhorar ainda mais nossas traduções? Confira <0>Crowdin</0> para mais detalhes.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Deseja\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Aviso (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Limites de aviso\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Notificações Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Quando ativado, este token permite que os agentes se registrem automaticamente sem criação prévia do sistema.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Ao usar POST, cada heartbeat inclui um payload JSON com o resumo do estado do sistema, a lista de sistemas inativos e os alertas acionados.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Comando Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Escrever\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Configuração YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Configuração YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Sim\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"As configurações do seu usuário foram atualizadas.\"\n"
  },
  {
    "path": "internal/site/src/locales/ru/ru.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: ru\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Russian\\n\"\n\"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: ru\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"Выбрано {0} из {1} строк.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# ядро} few {# ядра} many {# ядер} other {# ядер}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} день} other {{countString} дней}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} час} other {{countString} часов}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} минута} few {{countString} минут} many {{countString} минут} other {{countString} минуты}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# поток} few {# потока} many {# потоков} other {# потоков}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 час\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 мин\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 минута\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 неделя\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 часов\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 мин\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 часа\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 дней\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 мин\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Действия\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Активно\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Активные оповещения\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Активное состояние\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Добавить {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Добавить <0>Систему</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Добавить систему\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Добавить URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Настроить параметры отображения для графиков.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Настроить ширину основного макета\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Администратор\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"После\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"После установки переменных окружения перезапустите хаб Beszel, чтобы изменения вступили в силу.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Агент\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"История оповещений\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Оповещения\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Все контейнеры\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Все системы\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Вы уверены, что хотите удалить {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Вы уверены?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Автоматическое копирование требует безопасного контекста.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Среднее\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Среднее использование CPU контейнерами\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Среднее опускается ниже <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Среднее превышает <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Среднее потребление мощности всеми GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Среднее использование CPU по всей системе\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Среднее использование {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Средняя загрузка GPU движков\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Резервные копии\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Пропускная способность\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Батарея\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Батарея\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Стал активным\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Стал неактивным\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"До\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Ниже {0}{1} за последние {2, plural, one {# минуту} other {# минут}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel поддерживает OpenID Connect и множество поставщиков аутентификации OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel использует <0>Shoutrrr</0> для интеграции с популярными сервисами уведомлений.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Двоичный\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Биты (Кбит/с, Мбит/с, Гбит/с)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Состояние загрузки\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Байты (Кбайт/с, Мбайт/с, Гбайт/с)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Кэш / Буферы\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Может перезагрузить\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Может запустить\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Может остановить\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Отмена\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Возможности\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Емкость\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Внимание - возможная потеря данных\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Цельсий (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Изменить единицы измерения для метрик.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Изменить общие параметры приложения.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Заряд\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Заряжается\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Параметры графиков\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Проверьте {email} для получения ссылки на сброс.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Проверьте журналы для получения более подробной информации.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Проверьте ваш сервис мониторинга\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Проверьте ваш сервис уведомлений\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Очистить\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Нажмите на контейнер, чтобы просмотреть дополнительную информацию.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Нажмите на устройство для просмотра дополнительной информации.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Нажмите на систему для просмотра дополнительной информации.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Нажмите, чтобы скопировать\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Инструкции командной строки\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Настройте, как вы получаете уведомления об оповещениях.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Подтвердите пароль\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Конфликты\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Нет соединения\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Продолжить\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Скопировано в буфер обмена\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Скопировать docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Скопировать docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Скопировать env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Копировать хост\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Копировать команду Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Скопировать имя\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Копировать текст\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Скопируйте команду для установки агента ниже, или зарегистрируйте агентов автоматически с помощью <0>универсального токена</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Скопируйте содержимое <0>docker-compose.yml</0> для агента ниже, или зарегистрируйте агентов автоматически с помощью <1>универсального токена</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Скопировать YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"ЦП\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Ядра ЦП\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Пик CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Время CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Распределение времени ЦП\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Использование CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Создать\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Создать аккаунт\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Создано\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Критический (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Совокупная загрузка\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Совокупная отдача\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Текущее состояние\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Циклы\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Ежедневно\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Период по умолчанию\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Удалить\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Удалить отпечаток\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Описание\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Подробности\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Устройство\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Разряжается\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Диск\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Дисковый ввод/вывод\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Единицы измерения дисковой активности\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Использование диска\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Использование диска {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Использование CPU Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Использование памяти Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Сетевой ввод/вывод Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Документация\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Не в сети\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Не в сети ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Загрузка\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Длительность\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Редактировать\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Редактировать {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Электронная почта\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Уведомления по электронной почте\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Пустая\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Время окончания\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL-адрес конечной точки\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL-адрес конечной точки для пинга (обязательно)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Введите адрес электронной почты для сброса пароля\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Введите адрес электронной почты...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Введите ваш одноразовый пароль.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Эфемерный\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Ошибка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Пример:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Превышает {0}{1} за последние {2, plural, one {# минуту} other {# минут}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Основной PID процесса\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Существующие системы, не определенные в <0>config.yml</0>, будут удалены. Пожалуйста, делайте регулярные резервные копии.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Завершился активным\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Истекает через час или при перезапуске хаба.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Экспорт\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Экспорт конфигурации\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Экспортируйте текущую конфигурацию систем.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Фаренгейт (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Неудачно\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Неудачные атрибуты:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Не удалось аутентифицировать\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Не удалось сохранить настройки\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Не удалось отправить heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Не удалось отправить тестовое уведомление\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Не удалось обновить оповещение\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Неудачно: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Фильтр...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Отпечаток\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Прошивка\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"На <0>{min}</0> {min, plural, one {минуту} other {минут}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Забыли пароль?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Команда FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Полная\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Общие\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Глобально\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU движки\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Потребляемая мощность GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Использование GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Сетка\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Здоровье\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Мониторинг Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat успешно отправлен\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Команда Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Хост / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-метод\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-метод: POST, GET или HEAD (по умолчанию: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Неактивная\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Если вы потеряли пароль от своей учетной записи администратора, вы можете сбросить его, используя следующую команду.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Образ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Неактивно\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Интервал\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Неверный адрес электронной почты.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Язык\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Макет\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Ширина макета\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Жизненный цикл\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"лимит\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Средняя загрузка\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Средняя загрузка за 15м\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Средняя загрузка за 1м\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Средняя загрузка за 5м\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Ср. загрузка\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Состояние загрузки\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Загрузка...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Выйти\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Вход\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Попытка входа не удалась\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Журналы\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Ищете, где создать оповещения? Нажмите на значки колокольчика <0/> в таблице систем.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Основной PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Управляйте предпочтениями отображения и уведомлений.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Инструкции по ручной настройке\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Макс 1 мин\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Память\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Лимит памяти\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Пик памяти\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Использование памяти\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Использование памяти контейнерами Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Модель\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Имя\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Сеть\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Сетевой трафик контейнеров Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Сетевой трафик публичных интерфейсов\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Единицы измерения скорости сети\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Нет\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Результаты не найдены.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Нет результатов.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Для этого устройства нет доступных атрибутов S.M.A.R.T.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Системы не найдены.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Уведомления\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Поддержка OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"При каждом перезапуске системы в базе данных будут обновлены в соответствии с системами, определенными в файле.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Одноразово\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Одноразовый пароль\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Открыть меню\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Или продолжить с\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Другое\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Перезаписать существующие оповещения\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Страница\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Страница {0} из {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Страницы / Настройки\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Пароль\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Пароль должен содержать не менее 8 символов.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Пароль должен быть меньше 72 символов.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Запрос на сброс пароля получен\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Прошлое\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Пауза\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Пауза\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Пауза ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Формат полезной нагрузки\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Среднее использование на ядро\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Процент времени, проведенного в каждом состоянии\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Постоянный\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Устойчивость\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Пожалуйста, <0>настройте SMTP-сервер</0>, чтобы гарантировать доставку оповещений.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Пожалуйста, проверьте журналы для получения более подробной информации.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Пожалуйста, проверьте свои учетные данные и попробуйте снова\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Пожалуйста, создайте учетную запись администратора\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Пожалуйста, включите всплывающие окна для этого сайта\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Пожалуйста, войдите снова\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Пожалуйста, смотрите <0>документацию</0> для получения инструкций.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Пожалуйста, войдите в свою учетную запись\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Порт\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Включение питания\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Точное использование в записанное время\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Предпочтительный язык\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Процесс запущен\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Ключ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Тихие часы\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Чтение\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Получено\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Обновить\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Связи\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Запросить одноразовый пароль\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Запросить OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Требуется для\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Требует\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Сбросить пароль\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Завершено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Перезапуски\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Возобновить\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Корневой\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Обновить токен\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Строк на странице\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Метрики времени выполнения\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Детали S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Самотестирование S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Сохраните адрес, используя клавишу ввода или запятую. Оставьте пустым, чтобы отключить уведомления по электронной почте.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Сохранить настройки\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Сохранить систему\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Сохранено в базе данных и не истекает, пока вы его не отключите.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Расписание\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Запланируйте тихие часы, когда уведомления не будут отправляться, например, во время обслуживания.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Запланируйте тихие часы, когда уведомления не будут отправляться.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Поиск\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Поиск систем или настроек...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Секунды между пингами (по умолчанию: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Смотрите <0>настройки уведомлений</0>, чтобы настроить, как вы получаете оповещения.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Выбрать {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Отправьте одиночный пинг heartbeat, чтобы проверить работоспособность конечной точки.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Отправляйте периодические исходящие пинги на внешний сервис мониторинга, чтобы вы могли мониторить Beszel без его публикации в интернете.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Отправить тестовый heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Отправлено\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Серийный номер\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Детали сервиса\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Службы\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Установите процентные пороги для цветов счетчиков.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Установите следующие переменные окружения в хабе Beszel для включения мониторинга heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Настройки\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Настройки сохранены\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Войти\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Настройки SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Сортировать по\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Время начала\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Состояние\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Статус\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Подсостояние\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Используемое системой пространство подкачки\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Использование подкачки\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Система\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Средняя загрузка системы за время\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Сервисы Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Системы\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Системы могут управляться в файле <0>config.yml</0> внутри вашего каталога данных.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Таблица\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Задачи\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Темп\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Температура\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Единицы измерения температуры\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Температуры датчиков системы\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Тест <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Тестовый heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Тестовое уведомление отправлено\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Общий статус: <0>ok</0>, когда все системы работают, <1>предупреждение</1>, когда сработали оповещения, и <2>ошибка</2>, когда какая-либо система отключена.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Затем войдите в бэкенд и сбросьте пароль вашей учетной записи в таблице пользователей.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Это действие не может быть отменено. Это навсегда удалит все текущие записи для {name} из базы данных.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Это навсегда удалит все выбранные записи из базы данных.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Пропускная способность {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Пропускная способность корневой файловой системы\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Формат времени\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"На электронную почту\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Переключить сетку\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Переключить тему\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Токен\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Токены и отпечатки\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Токены позволяют агентам подключаться и регистрироваться. Отпечатки являются постоянными идентификаторами, уникальными для каждой системы, которые создаются при первом подключении.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Токены и отпечатки используются для аутентификации соединений WebSocket с хабом.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Итого\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Общий объем полученных данных для каждого интерфейса\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Общий объем отправленных данных для каждого интерфейса\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Всего: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Запущено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Триггеры\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Срабатывает, когда средняя загрузка за 1 минуту превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Срабатывает, когда средняя загрузка за 15 минут превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Срабатывает, когда средняя загрузка за 5 минут превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Срабатывает, когда любой датчик превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Срабатывает, когда заряд батареи опускается ниже порога\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Срабатывает, когда комбинированный вход/выход превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Срабатывает, когда использование CPU превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Срабатывает, когда использование GPU превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Срабатывает, когда использование памяти превышает порог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Срабатывает, когда статус переключается между включено и выключено\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Срабатывает, когда использование любого диска превышает порог\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Тип\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Unit файл\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Параметры единиц измерения\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Универсальный токен\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Неизвестная\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Неограниченно\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"В сети\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"В сети ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Обновить\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Обновлено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Обновляется каждые 10 минут.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Отдача\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Использование\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Использование корневого раздела\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Использовано\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Пользователи\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Значение\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Вид\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Показать больше\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Просмотреть 200 последних оповещений.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Видимые столбцы\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Ожидание достаточного количества записей для отображения\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Хотите помочь нам улучшить наши переводы? Посетите <0>Crowdin</0> для получения более подробной информации.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Требует\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Предупреждение (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Пороги предупреждения\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push уведомления\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"При включении этот токен позволяет агентам самостоятельно регистрироваться без предварительного создания системы.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"При использовании POST каждый heartbeat содержит полезную нагрузку JSON с кратким отчетом о состоянии системы, списком отключенных систем и сработавшими оповещениями.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Команда Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Запись\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML конфигурация\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML конфигурация\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Да\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Ваши настройки пользователя были обновлены.\"\n"
  },
  {
    "path": "internal/site/src/locales/sl/sl.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: sl\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Slovenian\\n\"\n\"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: sl\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} od {1} vrstic izbranih.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# jedro} two {# jedri} few {# jedra} other {# jeder}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dan} two {{countString} dneva} few {{countString} dni} other {{countString} dni}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} ura} two {{countString} uri} few {{countString} ur} other {{countString} ur}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minuta} few {{countString} minuti} many {{countString} minut} other {{countString} minut}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# nit} two {# niti} few {# niti} other {# niti}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 ura\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minuta\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 teden\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 ur\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 ur\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dni\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Dejanja\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktivno\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktivna opozorila\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktivno stanje\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Dodaj {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Dodaj <0>sistem</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Dodaj sistem\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Dodaj URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Prilagodi možnosti prikaza za grafikone.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Prilagodite širino glavne postavitve\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Administrator\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Po\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Po nastavitvi okoljskih spremenljivk ponovno zaženite vozlišče Beszel, da spremembe začnejo veljati.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Zgodovina opozoril\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Opozorila\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Vsi kontejnerji\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Vsi sistemi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Ali ste prepričani, da želite izbrisati {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Ali ste prepričani?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Za samodejno kopiranje je potreben varen kontekst.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Povprečno\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Povprečna izkoriščenost procesorja kontejnerjev\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Povprečje pade pod <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Povprečje presega <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Povprečna poraba energije GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Povprečna CPU izkoriščenost v celotnem sistemu\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Povprečna poraba {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Povprečna uporaba GPU motorjev\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Varnostne kopije\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Pasovna širina\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Baterija\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Postalo aktivno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Postalo neaktivno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Pred\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Pod {0}{1} v zadnjih {2, plural, one {# minuti} two {# minutah} few {# minutah} other {# minutah}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel podpira OpenID Connect in številne ponudnike preverjanja pristnosti OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel uporablja <0>Shoutrrr</0> za integracijo s priljubljenimi storitvami obveščanja.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binarno\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Biti (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Stanje zagona\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bajti (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Predpomnilnik / medpomnilniki\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Lahko ponovno naloži\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Lahko zažene\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Lahko ustavi\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Prekliči\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Zmožnosti\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapaciteta\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Pozor - možna izguba podatkov\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celzija (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Spremenite enote prikaza za metrike.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Spremeni splošne možnosti aplikacije.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Naboj\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Polni se\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Možnosti grafikona\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Preverite {email} za povezavo za ponastavitev.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Za več podrobnosti preverite dnevnike.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Preverite svojo storitev za spremljanje\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Preverite storitev obveščanja\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Počisti\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Kliknite na kontejner za več informacij.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Kliknite na napravo za več informacij.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Kliknite na sistem za več informacij.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klikni za kopiranje\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Navodila za ukazno vrstico\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Nastavi način prejemanja opozorilnih obvestil.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Potrdite geslo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikti\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Povezava je prekinjena\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Nadaljuj\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Kopirano v odložišče\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopiraj docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Kopiraj docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopiraj okoljske spremenljivke\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopiraj gostitelja\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopiraj Linux ukaz\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopiraj ime\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopiraj besedilo\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopirajte namestitveni ukaz za agenta spodaj ali registrirajte agente samodejno z <0>univerzalnim žetonom</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopirajte<0>docker-compose.yml</0> vsebino za agenta spodaj ali registrirajte agente samodejno z <1>univerzalnim žetonom</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopiraj YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPE\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU jedra\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Vrhunec CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Čas CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Razčlenitev časa CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU poraba\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Ustvari\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Ustvari račun\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Ustvarjeno\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritično (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativno prenos\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativno nalaganje\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Trenutno stanje\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cikli\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Dnevno\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Privzeto časovno obdobje\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Izbriši\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Izbriši prstni odtis\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Opis\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Podrobnost\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Naprava\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Prazni se\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk V/I\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Enota diska\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Poraba diska\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Poraba diska za {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU poraba\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker poraba spomina\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker I/O mreže\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentacija\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nedelujoč\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nedelujoči ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Prenesi\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Trajanje\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Uredi\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Uredi {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-pošta\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-poštna obvestila\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Prazna\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Čas konca\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL končne točke\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL končne točke za ping (zahtevano)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Vnesite e-poštni naslov za ponastavitev gesla\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Vnesite e-poštni naslov...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Vnesite svoje enkratno geslo.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Prehodni\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Napaka\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Primer:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Preseženo {0}{1} v zadnjih {2, plural, one {# minuti} other {# minutah}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Glavni PID izvajanja\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Obstoječi sistemi, ki niso definirani v <0>config.yml</0>, bodo izbrisani. Prosimo, naredite redne varnostne kopije.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Izhod aktivno\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Poteče po eni uri ali ob ponovnem zagonu huba.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Izvozi\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Izvozi nastavitve\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Izvozi trenutne nastavitve sistema.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Neuspešno\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Neuspeli atributi:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Preverjanje pristnosti ni uspelo\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Shranjevanje nastavitev ni uspelo\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Slanje srčnega utripa (heartbeat) ni uspelo\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Pošiljanje testnega obvestila ni uspelo\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Opozorila ni bilo mogoče posodobiti\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Neuspešno: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtriraj...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Prstni odtis\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Vdelana programska oprema\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Za <0>{min}</0> {min, plural, one {minuto} other {minut}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Pozabljeno geslo?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Ukaz FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Polna\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Splošno\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Globalno\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU motorji\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU poraba moči\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Poraba GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Mreža\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Zdravje\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Srčni utrip (Heartbeat)\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Spremljanje srčnega utripa\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Srčni utrip uspešno poslan\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Ukaz Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Gostitelj / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Metoda HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Metoda HTTP: POST, GET ali HEAD (privzeto: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Neaktivna\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Če ste izgubili geslo za svoj skrbniški račun, ga lahko ponastavite z naslednjim ukazom.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Slika\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Neaktivno\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Napačen e-poštni naslov.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Jezik\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Postavitev\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Širina postavitve\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Življenjski cikel\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"omejitev\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Povprečna obremenitev\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Povprečna obremenitev 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Povprečna obremenitev 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Povprečna obremenitev 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Povpr. obrem.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Stanje nalaganja\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Nalaganje...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Odjava\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Prijava\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Poskus prijave ni uspel\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Dnevniki\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Namesto tega iščete, kje ustvariti opozorila? Kliknite ikone zvonca <0/> v sistemski tabeli.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Glavni PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Upravljajte nastavitve prikaza in obvestil.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Navodila za ročno nastavitev\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Največ 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Pomnilnik\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Omejitev pomnilnika\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Vrhunec pomnilnika\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Poraba pomnilnika\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Poraba pomnilnika docker kontejnerjev\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Naziv\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Mreža\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Omrežni promet docker kontejnerjev\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Omrežni promet javnih vmesnikov\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Enota omrežja\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Ne\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Ni rezultatov.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Ni rezultatov.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Za to napravo ni na voljo atributov S.M.A.R.T.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Ne najdem sistema.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Obvestila\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Podpora za OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Ob vsakem ponovnem zagonu bodo sistemi v zbirki podatkov posodobljeni, da se bodo ujemali s sistemi, definiranimi v datoteki.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Enkratno\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Enkratno geslo\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Odpri menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Ali nadaljuj z\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Drugo\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Prepiši obstoječe alarme\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Stran\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Stran {0} od {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Strani / Nastavitve\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Geslo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Geslo mora imeti vsaj 8 znakov.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Geslo mora biti krajše od 72 bajtov.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Prejeta zahteva za ponastavitev gesla\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Preteklo\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Premor\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Zaustavljeno\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pavzirano za {pausedSystemsLength}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Oblika tovora (payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Povprečna izkoriščenost na jedro\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Odstotek časa, preživetega v vsakem stanju\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Trajen\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Vztrajnost\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"<0>Nastavite strežnik SMTP</0>, da zagotovite dostavo opozoril.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Za več podrobnosti preverite dnevnike.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Preverite svoje poverilnice in poskusite znova\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Ustvarite skrbniški račun\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Omogočite pojavna okna za to spletno mesto\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Prosimo, prijavite se znova\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Za navodila glejte <0>dokumentacijo</0>.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Prijavite se v svoj račun\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Vrata\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Vklopljeno\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Natančna poraba v zabeleženem času\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Prednostni jezik\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Proces začet\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Javni ključ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Tihi čas\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Preberano\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Prejeto\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Osveži\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Razmerja\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Zahtevaj enkratno geslo\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Zahtevaj OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Zahtevano od\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Zahteva\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Ponastavi geslo\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Rešeno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Ponovni zagoni\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Nadaljuj\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Koren\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Zavrti žeton\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Vrstic na stran\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Metrike izvajanja\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. podrobnosti\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. samotestiranje\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Shranite naslov s tipko enter ali vejico. Pustite prazno, da onemogočite e-poštna obvestila.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Shrani nastavitve\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Shrani sistem\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Shranjeno v bazi podatkov in ne poteče, dokler ga ne onemogočite.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Razpored\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Razporedite tihi čas, ko obvestila ne bodo poslana, na primer med vzdrževalnimi obdobji.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Razporedite tihi čas, ko obvestila ne bodo poslana.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Iskanje\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Iskanje sistemov ali nastavitev...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunde med pingi (privzeto: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Glejte <0>nastavitve obvestil</0>, da nastavite način prejemanja opozoril.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Izberi {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Pošljite en srčni utrip (ping), da preverite, ali vaša končna točka deluje.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Pošiljajte občasne odhodne pinge zunanji storitvi za spremljanje, da boste lahko spremljali Beszel, ne da bi ga izpostavili internetu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Pošlji testni srčni utrip\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Poslano\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serijska številka\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Podrobnosti storitve\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Storitve\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Nastavite odstotne pragove za barve merilnikov.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Na svojem vozlišču Beszel nastavite naslednje okoljske spremenljivke, da omogočite spremljanje srčnega utripa:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Nastavitve\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Nastavitve so shranjene\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Prijavite se\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP nastavitve\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Razvrsti po\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Čas začetka\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Stanje\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Stanje\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Podstanje\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap prostor, ki ga uporablja sistem\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap uporaba\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistemsko\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Sistemske povprečne obremenitve skozi čas\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd storitve\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistemi\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Sisteme lahko upravljate v datoteki <0>config.yml</0> v vašem podatkovnem imeniku.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabela\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Naloge\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatura\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Enota temperature\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperature sistemskih senzorjev\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Preveri <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testni srčni utrip\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testno obvestilo je poslano\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Splošno stanje je <0>v redu</0>, ko vsi sistemi delujejo, <1>opozorilo</1>, ko se sprožijo opozorila, in <2>napaka</2>, ko kateri koli sistem ne deluje.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Nato se prijavite v zaledni sistem in ponastavite geslo svojega uporabniškega računa v tabeli uporabnikov.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Tega dejanja ni mogoče razveljaviti. To bo trajno izbrisalo vse trenutne zapise za {name} iz zbirke podatkov.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"To bo trajno izbrisalo vse izbrane zapise iz zbirke podatkov.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Prepustnost {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Prepustnost korenskega datotečnega sistema\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Oblika časa\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"E-pošta za\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Preklopi način mreže\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Obrni temo\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Žeton\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Žetoni in prstni odtisi\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Žetoni omogočajo agentom povezavo in registracijo. Prstni odtisi so stabilni identifikatorji, edinstveni za vsak sistem, nastavljeni ob prvi povezavi.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Žetoni in prstni odtisi se uporabljajo za preverjanje pristnosti WebSocket povezav do vozlišča.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Skupaj\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Skupni prejeti podatki za vsak vmesnik\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Skupni poslani podatki za vsak vmesnik\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Skupaj: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Sproženo z\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Sprožilci\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Sproži se, ko 1-minutna povprečna obremenitev preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Sproži se, ko 15-minutna povprečna obremenitev preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Sproži se, ko 5-minutna povprečna obremenitev preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Sproži se, ko kateri koli senzor preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Sproži se, ko napolnjenost baterije pade pod prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Sproži, ko kombinacija gor/dol preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Sproži se, ko poraba procesorja preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Sproži se, ko poraba GPU preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Sproži se, ko uporaba pomnilnika preseže prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Sproži se, ko se stanje preklaplja med gor in dol\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Sproži se, ko uporaba katerega koli diska preseže prag\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Vrsta\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Datoteka enote\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Nastavitve enot\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Univerzalni žeton\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Neznana\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Neomejeno\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Delujoč\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Delujoči ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Posodobi\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Posodobljeno\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Posodobljeno vsakih 10 minut.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Naloži\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Uporaba\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Uporaba korenske particije\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Uporabljeno\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Uporabniki\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Vrednost\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Pogled\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Prikaži več\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Oglejte si svojih 200 najnovejših opozoril.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Vidna polja\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Čakam na dovolj zapisov za prikaz\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Ali nam želite pomagati, da bomo naše prevode še izboljšali? Za več podrobnosti si oglejte <0>Crowdin</0>.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Zahteva\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Opozorilo (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Pragovi za opozorila\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / potisna obvestila\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Ko je omogočen, ta žeton omogoča agentom samoregistracijo brez predhodnega ustvarjanja sistema.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Pri uporabi POST vsak srčni utrip vključuje tovor JSON s povzetkom stanja sistema, seznamom nedelujočih sistemov in sproženimi opozorili.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Ukaz Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Pisanje\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML nastaviitev\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML nastavitev\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Da\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Vaše uporabniške nastavitve so posodobljene.\"\n"
  },
  {
    "path": "internal/site/src/locales/sr/sr.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: sr\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-02-03 15:27\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Serbian (Cyrillic)\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: sr\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} од {1} редова изабрано.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# jezgro} few {# jezgra} other {# jezgara}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} дан} few {{countString} дана} other {{countString} дана}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} сат} few {{countString} сата} other {{countString} сати}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} минут} few {{countString} минута} many {{countString} минута} other {{countString} минута}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# nit} few {# niti} other {# niti}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 сат\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 мин\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 минут\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 недеља\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 сати\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 мин\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 сата\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 дана\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 мин\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Акције\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Активно\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Активна упозорења\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Активно стање\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Додај {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Додај <0>систем</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Додај систем\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Додај URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Подесите опције приказа за графиконе.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Подесите ширину главног распореда\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Администратор\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"После\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Nakon podešavanja promenljivih okruženja, ponovo pokrenite Beszel hub da bi promene stupile na snagu.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Агент\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Историја упозорења\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Упозорења\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Сви контејнери\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Сви системи\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Да ли сте сигурни да желите да избришете {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Да ли сте сигурни?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Аутоматско копирање захтева безбедан контекст.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Просек\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Просечна искоришћеност процесора контејнера\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Prosek pada ispod <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Просек премашује <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Просечна потрошња енергије графичких картица\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Просечна системска искоришћеност процесор\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Просечна употреба {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Просечна искоришћеност мотора графичких картица\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Резервне копије\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Пропусни опсег\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Бат\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Батерија\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Постао активан\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Постао неактиван\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Пре\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Ispod {0}{1} u poslednjih {2, plural, one {# minuti} few {# minuta} other {# minuta}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel подржава OpenID Connect и многе OAuth2 провајдере аутентификације.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel користи <0>Shoutrrr</0> за интеграцију са популарним сервисима за обавештавања.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Бинарни\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Битови (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Стање покретања\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Бајтови (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Кеш / Бафери\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Може се поново учитати\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Може се покренути\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Може се зауставити\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Откажи\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Могућности\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Капацитет\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Пажња - потенцијални губитак података\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Целзијус (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Промените јединице приказа за метрике.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Промените опште опције апликације.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Напуњеност\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Пуни се\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Опције графикона\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Проверите {email} за линк за ресетовање.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Проверите логове за више детаља.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Proverite svoju uslugu monitoringa\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Проверите ваш сервис за обавештавања\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Обриши\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Кликните на контејнер да видите више информација.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Кликните на уређај да видите више информација.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Кликните на систем да видите више информација.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Кликните да копирате\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Инструкције командне линије\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Конфигуришите како primate обавештења о упозорењима.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Потврдите лозинку\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Конфликти\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Веза је прекинута\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Настави\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Копирано у међуспремник\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Копирај docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Копирај docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Копирај env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Копирај хоста\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Копирај Linux команду\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Копирај име\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Копирај текст\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Копирајте команду за инсталацију агента испод, или региструјте агенте аутоматски са <0>универзалним токеном</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Копирајте садржај<0>docker-compose.yml</0> датотеке за агента испод, или региструјте агенте аутоматски са <1>универзалним токеном</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Копирај YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"Процесор\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU језгра\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU врхунац\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU време\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Расподела времена процесора\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Искоришћеност процесора\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Креирај\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Креирај налог\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Креирано\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Критично (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Кумулативно преузимање\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Кумулативно отпремање\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Тренутно стање\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Циклуси\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Дневно\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Подразумевани временски период\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Избриши\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Избриши отисак\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Опис\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Детаљ\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Уређај\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Празни се\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Диск\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Диск I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Диск јединица\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Употреба диска\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Употреба диска {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU употреба\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker употреба меморије\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker мрежни I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Документација\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Искључен\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Искључен ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Преузимање\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Трајање\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Измени\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Измени {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Е-пошта\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Обавештењe е-поштом\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Празнo\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Време завршетка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL krajnje tačke\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL krajnje tačke za ping (obavezno)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Унесите адресу е-поште за ресетовање лозинке\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Унесите адресу е-поште...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Унесите вашу једнократну лозинку.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Ефемеран\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Грешка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Primer:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Премашује {0}{1} у последњих {2, plural, one {# минуту} few {# минута} other {# минута}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Главни PID извршавања\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Системи који нису дефинисани у <0>config.yml</0> биће обрисани. Молимо вас да редовно правите резервне копије.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Излазак активан\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Истиче након једног сата или при поновном покретању хаба.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Извези\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Извези конфигурацију\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Извезите тренутну конфигурацију система.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Фаренхајт (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Неуспело\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Неуспели атрибути:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Неуспела аутентификација\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Неуспело чување подешавања\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Slanje heartbeat-a nije uspelo\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Неуспело слање тест обавештења\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Неуспело ажурирање упозорења\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Неуспело: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Филтрирај...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Отисак\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Фирмвер\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"За <0>{min}</0> {min, plural, one {минуту} few {минута} other {минута}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Заборављена лозинка?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD команда\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Пуно\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Опште\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Глобално\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU енгине\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU потрошња енергије\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU употреба\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Мрежа\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Здравље\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Monitoring heartbeat-a\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat uspešno poslat\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew команда\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Хост / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP metod\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP metod: POST, GET ili HEAD (podrazumevano: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Неактивно\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Ако сте изгубили лозинку за ваш администраторски налог, можете је ресетовати користећи следећу команду.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Слика\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Неактивно\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Interval\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Неважећа имејл адреса.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Језик\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Распоред\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Ширина распореда\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Животни циклус\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"ограничење\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Просечно оптерећење\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Просечно оптерећење 15м\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Просечно оптерећење 1м\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Просечно оптерећење 5м\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Просечно опт.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Стање учитавања\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Учитавање...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Одјави се\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Пријава\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Неуспешан покушај пријаве\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Логови\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Тражите где да креирате упозорења? Кликните на звоно <0/> у табели система.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Главни PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Управљајте преференцама приказа и обавештења.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Упутства за ручно подешавање\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Макс 1 мин\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Меморија\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Ограничење меморије\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Врхунац меморије\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Употреба меморије\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Употреба меморије docker контејнера\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Модел\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Име\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Мрежа\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Мрежни саобраћај docker контејнера\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Мрежни саобраћај јавних интерфејса\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Мрежна јединица\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Не\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Нису пронађени резултати.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Нема резултата.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Нема доступних S.M.A.R.T. атрибута за овај уређај.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Нису пронађени системи.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Обавештења\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC подршка\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"При сваком поновном покретању, системи у бази података ће се ажурирати да одговарају системима дефинисаним у датотеци.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Једнократно\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Једнократна лозинка\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Отвори мени\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Или наставите са\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Остало\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Препиши постојећа упозорења\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Страница\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Страница {0} од {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Странице / Подешавања\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Лозинка\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Лозинка мора имати најмање 8 карактера.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Лозинка мора бити мања од 72 бајта.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Захтев за ресетовање лозинке примљен\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Прошлост\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Паузирај\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Паузирано\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Паузирано ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Format korisnog opterećenja (payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Просечна употреба по језгру\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Проценат времена проведеног у сваком стању\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Трајан\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Упорност\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Молимо вас <0>конфигуришите SMTP сервер</0> да бисте осигурали да се упозорења испоручују.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Молимо вас проверите логове за више детаља.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Молимо вас проверите своје податке за пријаву и покушајте поново\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Молимо вас креирајте администраторски налог\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Молимо вас омогућите искачуће прозоре за овај сајт\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Молимо вас да се пријавите поново\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Молимо вас погледајте <0>документацију</0> за упутства.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Молимо вас да се пријавите на ваш налог\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Порт\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Укључи\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Прецизна употреба у тренутку снимања\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Жељени језик\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Процес покренут\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Јавни кључ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Тихи сати\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Читање\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Примљено\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Освежи\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Односи\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Затражи једнократну лозинку\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Затражи OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Захтевано од\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Захтева\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Ресетуј лозинку\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Решено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Поновна покретања\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Настави\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Ротирај токен\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Редова по страници\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Метрике у време извршавања\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. детаљи\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. самопровера\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Сачувајте адресу користећи enter тастер или зарез. Оставите празно да онемогућите обавештења путем е -поште.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Сачувај подешавања\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Сачувај систем\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Сачувано у бази података и не истиче док га не онемогућите.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Распоред\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Закажите тихе сате када се обавештења неће слати, као што су периоди одржавања.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Закажите тихе сате када се обавештења неће слати.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Претрага\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Претражите системе или подешавања...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunde između pingova (podrazumevano: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Погледајте <0>подешавања обавештења</0> да конфигуришете како primate упозорења.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Изаберите {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Pošaljite jedan heartbeat ping da biste proverili da li krajnja tačka radi.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Šaljite periodične odlazne pingove spoljnoj usluzi monitoringa kako biste mogli da nadgledate Beszel bez izlaganja internetu.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Pošalji testni heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Послато\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Серијски број\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Детаљи сервиса\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Сервиси\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Подесите процентуалне прагове за боје мерача.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Podesite sledeće promenljive okruženja na svom Beszel hub-u da biste omogućili monitoring heartbeat-a:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Подешавања\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Подешавања сачувана\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Пријави се\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP подешавања\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Сортирај по\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Време почетка\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Стање\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Статус\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Подстање\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap простор који користи систем\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap употреба\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Систем\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Просечна оптерећења система током времена\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd сервиси\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Системи\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Системи се могу управљати у <0>config.yml</0> датотеци у вашем директоријуму података.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Табела\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Задаци\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Темп\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Температура\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Јединица температуре\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Температуре системских сензора\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Тестирај <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testiraj heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Тест обавештење послато\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Ukupan status je <0>ok</0> kada su svi sistemi aktivni, <1>upozorenje</1> kada se aktiviraju upozorenja i <2>greška</2> kada je bilo koji sistem isključen.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Затим се пријавите на backend и ресетујте лозинку корисничког налога у табели корисника.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Ова акција се не може опозвати. Ово ће трајно избрисати све тренутне записе за {name} из базе података.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Ово ће трајно избрисати све изабране записе из базе података.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Проток {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Проток root фајл система\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Формат времена\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"На е-пошту(е)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Укључи/искључи мрежу\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Промени тему\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Токен\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Токени и отисци\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Токени омогућавају агентима да се повежу и региструју. Отисци су стабилни идентификатори јединствени за сваки систем, постављени при првом повезивању.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Токени и отисци се користе за аутентификацију WebSocket веза са хабом.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Укупно\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Укупни подаци примљени за сваки интерфејс\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Укупни подаци poslati за сваки интерфејс\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Укупно: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Окинуто од\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Окидачи\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Окида се када просечно оптерећење од 1 минут премаши prag\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Окида се када просечно оптерећење од 15 минута премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Окида се када просечно оптерећење од 5 минута премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Окида се када било који сензор премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Pokreće se kada nivo baterije padne ispod praga\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Окида се када комбиновано горе/доле премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Окида се када CPU употреба премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Окида се када GPU употреба премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Окида се када употреба меморије премаши праг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Окида се када статус прелази између укљученог и искљученог\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Окида се када употреба било ког диска премаши праг\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Тип\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Јединична датотека\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Преференце јединица\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Универзални токен\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Непознато\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Неограничено\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Укључен\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Укључен ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Ажурирај\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Ажурирано\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Ажурира се сваких 10 минута.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Отпреми\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Употреба\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Употреба root партиције\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Коришћено\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Корисници\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Вредност\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Погледај\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Погледај више\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Погледајте ваших 200 најновијих упозорења.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Видљива поља\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Чека се довољно записа за приказ\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Желите да помогнете у побољшању наших превода? Проверите <0>Crowdin</0> за детаље.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Жели\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Упозорење (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Прагове упозорења\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push обавештења\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Када је омогућен, овај токен омогућава агентима да се сами региструју без претходног креирања система.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Kada koristite POST, svaki heartbeat uključuje JSON payload sa rezimeom statusa sistema, listom isključenih sistema i aktiviranim upozorenjima.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows команда\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Писање\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML конфигурација\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML конфигурација\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Да\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Ваша корисничка подешавања су ажурирана.\"\n"
  },
  {
    "path": "internal/site/src/locales/sv/sv.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: sv\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Swedish\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: sv-SE\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{0} av {1} rad(er) valda.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# kärna} other {# kärnor}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} dag} other {{countString} dagar}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} timme} other {{countString} timmar}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} minut} few {{countString} minuter} many {{countString} minuter} other {{countString} minuter}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# tråd} other {# trådar}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 timme\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 min\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 minut\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 vecka\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 timmar\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 min\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 timmar\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 dagar\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 min\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Åtgärder\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktiv\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktiva larm\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktivt tillstånd\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Lägg till {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Lägg till <0>System</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Lägg till system\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Lägg till URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Justera visningsalternativ för diagram.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Justera bredden på huvudlayouten\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Admin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Efter\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Efter att du har ställt in miljövariablerna, starta om din Beszel-hubb för att ändringarna ska träda i kraft.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Larmhistorik\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Larm\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Alla behållare\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Alla system\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Är du säker på att du vill ta bort {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Är du säker?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Automatisk kopiering kräver en säker kontext.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Genomsnitt\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Genomsnittlig CPU-användning för containrar\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Genomsnittet sjunker under <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Genomsnittet överskrider <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Genomsnittlig strömförbrukning för GPU:er\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Genomsnittlig systemomfattande CPU-användning\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Genomsnittlig användning av {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Genomsnittlig användning av GPU-motorer\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Säkerhetskopior\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bandbredd\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Batteri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Blev aktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Blev inaktiv\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Före\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Under {0}{1} under de senaste {2, plural, one {# minuten} other {# minuterna}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel stöder OpenID Connect och många OAuth2-autentiseringsleverantörer.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel använder <0>Shoutrrr</0> för att integrera med populära aviseringstjänster.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Binär\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bit (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Starttillstånd\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bytes (KB/s, MB/s, GB/S)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Cache / Buffertar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Kan ladda om\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Kan starta\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Kan stoppa\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Avbryt\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Capabilities\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapacitet\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Varning - potentiell dataförlust\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Celsius (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Ändra enheter för mätvärden.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Ändra allmänna programalternativ.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Laddning\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Laddar\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Diagramalternativ\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Kontrollera {email} för en återställningslänk.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Kontrollera loggarna för mer information.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Kontrollera din övervakningstjänst\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Kontrollera din aviseringstjänst\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Rensa\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Klicka på en behållare för att visa mer information.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Klicka på en enhet för att visa mer information.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Klicka på ett system för att visa mer information.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Klicka för att kopiera\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Instruktioner för kommandoraden\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Konfigurera hur du tar emot larmaviseringar.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Bekräfta lösenord\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Konflikter\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Ej ansluten\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Fortsätt\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Kopierat till urklipp\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Kopiera docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Kopiera docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Kopiera env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Kopiera värd\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Kopiera Linux-kommando\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Kopiera namn\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Kopiera text\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Kopiera installationskommandot för agenten nedan, eller registrera agenter automatiskt med en <0>universal token</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Kopiera <0>docker-compose.yml</0>-innehållet för agenten nedan, eller registrera agenter automatiskt med en <1>universal token</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Kopiera YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU-kärnor\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU-topp\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU-tid\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU-tidsuppdelning\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU-användning\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Skapa\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Skapa konto\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Skapad\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritisk (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kumulativ nedladdning\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kumulativ uppladdning\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Aktuellt tillstånd\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Cykler\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Dagligen\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Standardtidsperiod\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Ta bort\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Ta bort fingeravtryck\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Beskrivning\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Detalj\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Enhet\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Urladdar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Diskenhet\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Diskanvändning\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Diskanvändning av {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU-användning\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker Minnesanvändning\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Nätverks-I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokumentation\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Nere\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Nere ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Ladda ner\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Varaktighet\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Redigera\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Redigera {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-post\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-postaviseringar\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Tom\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Sluttid\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Slutpunkts-URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Slutpunkts-URL att pinga (krävs)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Ange e-postadress för att återställa lösenord\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Ange e-postadress...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Ange ditt engångslösenord.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Flyktig\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Fel\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Exempel:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Överskrider {0}{1} under de senaste {2, plural, one {# minuten} other {# minuterna}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Exec-huvud-PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Befintliga system som inte definieras i <0>config.yml</0> kommer att tas bort. Gör regelbundna säkerhetskopior.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Avslutades aktivt\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Upphör efter en timme eller vid hub-omstart.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Exportera\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Exportera konfiguration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Exportera din nuvarande systemkonfiguration.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenheit (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Misslyckades\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Misslyckade attribut:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Autentisering misslyckades\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Kunde inte spara inställningar\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Misslyckades med att skicka heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Kunde inte skicka testavisering\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Kunde inte uppdatera larm\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Misslyckades: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtrera...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingeravtryck\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Fast programvara\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Under <0>{min}</0> {min, plural, one {minut} other {minuter}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Glömt lösenordet?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD kommando\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Full\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Allmänt\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Global\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU-motorer\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU-strömförbrukning\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU-användning\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Rutnät\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Hälsa\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat-övervakning\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat skickades framgångsrikt\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew-kommando\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Värd / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-metod\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-metod: POST, GET eller HEAD (standard: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Vilande\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Om du har glömt lösenordet till ditt administratörskonto kan du återställa det med följande kommando.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Avbild\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Inaktiv\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Intervall\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Ogiltig e-postadress.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Språk\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Layout\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Layoutbredd\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Livscykel\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"gräns\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Genomsnittlig belastning\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Genomsnittlig belastning 15m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Genomsnittlig belastning 1m\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Genomsnittlig belastning 5m\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Belastning\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Laddningstillstånd\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Laddar...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Logga ut\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Logga in\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Inloggningsförsök misslyckades\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Loggar\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Letar du istället efter var du skapar larm? Klicka på klockikonerna <0/> i systemtabellen.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Huvud-PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Hantera visnings- och aviseringsinställningar.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Manuella installationsinstruktioner\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Max 1 min\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Minne\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Minnesgräns\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Minnestopp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Minnesanvändning\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Minnesanvändning för dockercontainrar\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Modell\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Namn\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Nät\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Nätverkstrafik för dockercontainrar\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Nätverkstrafik för publika gränssnitt\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Nätverksenhet\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Nej\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Inga resultat hittades.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Inga resultat.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Inga S.M.A.R.T.-attribut tillgängliga för den här enheten.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Inga system hittades.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Aviseringar\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Stöd för OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Vid varje omstart kommer systemen i databasen att uppdateras för att matcha systemen som definieras i filen.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Engångs\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Engångslösenord\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Öppna menyn\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Eller fortsätt med\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Annat\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Skriv över befintliga larm\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Sida\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Sida {0} av {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Sidor / Inställningar\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Lösenord\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Lösenordet måste vara minst 8 tecken.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Lösenordet måste vara mindre än 72 byte.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Begäran om återställning av lösenord mottagen\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Förbi\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Paus\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Pausad\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Pausad ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Nyttolastformat\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Genomsnittlig användning per kärna\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Procentandel av tid spenderad i varje tillstånd\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Permanent\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Beständighet\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Vänligen <0>konfigurera en SMTP-server</0> för att säkerställa att larm levereras.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Vänligen kontrollera loggarna för mer information.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Vänligen kontrollera dina inloggningsuppgifter och försök igen\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Vänligen skapa ett administratörskonto\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Vänligen aktivera popup-fönster för den här webbplatsen\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Vänligen logga in igen\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Vänligen se <0>dokumentationen</0> för instruktioner.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Vänligen logga in på ditt konto\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Påslagen\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Exakt användning vid den registrerade tidpunkten\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Föredraget språk\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Process startad\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Offentlig nyckel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Tysta timmar\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Läs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Mottaget\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Uppdatera\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Relationer\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Begär engångslösenord\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Begär OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Krävs av\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Kräver\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Återställ lösenord\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Löst\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Omstarter\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Återuppta\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Rotera token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Rader per sida\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Körningsmätvärden\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T.-detaljer\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. själftest\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Spara adressen med Enter-tangenten eller komma. Lämna tomt för att inaktivera e-postaviseringar.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Spara inställningar\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Spara system\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Sparad i databasen och upphör inte förrän du inaktiverar den.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Schema\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Schemalägg tysta timmar där aviseringar inte skickas, till exempel under underhållsperioder.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Schemalägg tysta timmar där aviseringar inte skickas.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Sök\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Sök efter system eller inställningar...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Sekunder mellan pingar (standard: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Se <0>aviseringsinställningar</0> för att konfigurera hur du tar emot larm.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Välj {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Skicka en enstaka heartbeat-ping för att verifiera att din slutpunkt fungerar.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Skicka periodiska utgående pingar till en extern övervakningstjänst så att du kan övervaka Beszel utan att exponera den för internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Skicka test-heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Skickat\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Serienummer\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Tjänstedetaljer\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Tjänster\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Ställ in procentuella tröskelvärden för mätarfärger.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Ställ in följande miljövariabler på din Beszel-hubb för att aktivera heartbeat-övervakning:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Inställningar\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Inställningar sparade\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Logga in\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP-inställningar\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sortera efter\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Starttid\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Tillstånd\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Status\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Deltillstånd\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Swap-utrymme som används av systemet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Swap-användning\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"System\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Systembelastning över tid\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd-tjänster\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"System\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"System kan hanteras i en <0>config.yml</0>-fil i din datakatalog.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tabell\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Uppgifter\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Temp\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Temperatur\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Temperaturenhet\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Temperaturer för systemsensorer\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Testa <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Testa heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Testavisering skickad\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Den övergripande statusen är <0>ok</0> när alla system är uppe, <1>varning</1> när larm utlöses och <2>fel</2> när något system är nere.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Logga sedan in på backend och återställ ditt användarkontos lösenord i användartabellen.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Den här åtgärden kan inte ångras. Detta kommer permanent att ta bort alla aktuella poster för {name} från databasen.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Detta kommer permanent att ta bort alla valda poster från databasen.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Genomströmning av {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Genomströmning av rotfilsystemet\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Tidsformat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Till e-postadress(er)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Växla rutnät\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Växla tema\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Nyckel\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Tokens & fingeravtryck\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Tokens tillåter agenter att ansluta och registrera. Fingeravtryck är stabila identifierare unika för varje system, inställda vid första anslutning.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Tokens och fingeravtryck används för att autentisera WebSocket-anslutningar till hubben.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Total\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Totalt mottagen data för varje gränssnitt\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Totalt skickad data för varje gränssnitt\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Totalt: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Utlöst av\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Utlösare\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Utlöses när 1-minuters genomsnittlig belastning överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Utlöses när 15-minuters genomsnittlig belastning överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Utlöses när 5-minuters genomsnittlig belastning överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Utlöses när någon sensor överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Utlöses när batteriladdningen sjunker under ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Utlöses när kombinerad upp/ner överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Utlöses när CPU-användningen överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Utlöses när GPU-användning överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Utlöses när minnesanvändningen överskrider ett tröskelvärde\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Utlöses när status växlar mellan upp och ner\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Utlöses när användningen av någon disk överskrider ett tröskelvärde\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Typ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Unit-fil\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Enhetsinställningar\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Universell nyckel\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Okänd\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Obegränsad\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Upp\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Upp ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Uppdatera\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Uppdaterad\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Uppdateras var 10:e minut.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Ladda upp\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Drifttid\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Användning\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Användning av rotpartitionen\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Använt\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Användare\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Värde\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Visa\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Visa mer\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Visa dina 200 senaste larm.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Synliga fält\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Väntar på tillräckligt med poster att visa\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Vill du hjälpa oss att göra våra översättningar ännu bättre? Kolla in <0>Crowdin</0> för mer information.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Vill ha\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Varning (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Varningströsklar\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push-aviseringar\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"När aktiverad tillåter denna token agenter att självregistrera utan föregående systemskapande.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"När POST används inkluderar varje heartbeat en JSON-nyttolast med systemstatussammanfattning, lista över nere system och utlösta larm.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows-kommando\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Skriv\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML-konfiguration\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML-konfiguration\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Ja\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Dina användarinställningar har uppdaterats.\"\n"
  },
  {
    "path": "internal/site/src/locales/tr/tr.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: tr\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Turkish\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: tr\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"{1} satırdan {0} tanesi seçildi.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# çekirdek} other {# çekirdek}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} gün} other {{countString} gün}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} saat} other {{countString} saat}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} dakika} few {{countString} dakika} many {{countString} dakika} other {{countString} dakika}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# iş parçacığı} other {# iş parçacığı}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 saat\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 dk\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 dakika\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 hafta\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 saat\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 dk\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 saat\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 gün\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 dk\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Eylemler\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Aktif\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Aktif Uyarılar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Aktif durum\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"{foo} ekle\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"<0>Sistem</0> Ekle\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Sistem ekle\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"URL Ekle\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Grafikler için görüntüleme seçeneklerini ayarlayın.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Ana düzenin genişliğini ayarla\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Yönetici\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Sonra\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Ortam değişkenlerini ayarladıktan sonra, değişikliklerin etkili olması için Beszel hub'ınızı yeniden başlatın.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Aracı\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Uyarı Geçmişi\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Uyarılar\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Tüm Konteynerler\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Tüm Sistemler\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"{name} silmek istediğinizden emin misiniz?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Emin misiniz?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Otomatik kopyalama güvenli bir bağlam gerektirir.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Ortalama\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Konteynerlerin ortalama CPU kullanımı\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Ortalama <0>{value}{0}</0> altına düşüyor\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Ortalama <0>{value}{0}</0> aşıyor\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU ların ortalama güç tüketimi\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Sistem genelinde ortalama CPU kullanımı\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0} ortalama kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU motorlarının ortalama kullanımı\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Yedekler\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Bant Genişliği\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Pil\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Pil\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Aktif oldu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Pasif oldu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Önce\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Son {2, plural, one {# dakika} other {# dakika}} içinde {0}{1} altında\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel, OpenID Connect ve birçok OAuth2 kimlik doğrulama sağlayıcısını destekler.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel, popüler bildirim hizmetleriyle entegre olmak için <0>Shoutrrr</0> kullanır.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"İkili\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bit (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Önyükleme durumu\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Bayt (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Önbellek / Tamponlar\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Yeniden yüklenebilir\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Başlatılabilir\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Durdurulabilir\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"İptal\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Yetenekler\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Kapasite\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Dikkat - potansiyel veri kaybı\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Santigrat (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Metrikler için görüntüleme birimlerini değiştirin.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Genel uygulama seçeneklerini değiştirin.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Şarj\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Şarj oluyor\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Grafik seçenekleri\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Sıfırlama bağlantısı için {email} kontrol edin.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Daha fazla ayrıntı için günlükleri kontrol edin.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"İzleme servisinizi kontrol edin\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Bildirim hizmetinizi kontrol edin\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Temizle\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Daha fazla bilgi görüntülemek için bir konteynere tıklayın.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Daha fazla bilgi görüntülemek için bir cihaza tıklayın.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Daha fazla bilgi görmek için bir sisteme tıklayın.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Kopyalamak için tıklayın\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Komut satırı talimatları\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Uyarı bildirimlerini nasıl alacağınızı yapılandırın.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Şifreyi onayla\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Çakışmalar\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Bağlantı kesildi\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Devam et\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Panoya kopyalandı\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Docker compose kopyala\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Docker run kopyala\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Ortam değişkenlerini kopyala\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Ana bilgisayarı kopyala\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Linux komutunu kopyala\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Adı kopyala\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Metni kopyala\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Aşağıdaki agent için kurulum komutunu kopyalayın veya <0>evrensel token</0> ile agentları otomatik olarak kaydedin.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Aşağıdaki agent için <0>docker-compose.yml</0> içeriğini kopyalayın veya <1>evrensel token</1> ile agentları otomatik olarak kaydedin.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"YAML'ı kopyala\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU Çekirdekleri\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU Tepe Değeri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU zamanı\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU Zaman Dağılımı\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU Kullanımı\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Oluştur\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Hesap oluştur\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Oluşturuldu\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Kritik (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Kümülatif İndirme\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Kümülatif Yükleme\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Mevcut durum\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Döngüler\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Günlük\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Varsayılan zaman dilimi\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Sil\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Parmak izini sil\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Açıklama\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Ayrıntı\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Cihaz\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Boşalıyor\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Disk\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Disk G/Ç\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Disk birimi\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Disk Kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName} disk kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU Kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker Bellek Kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker Ağ G/Ç\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Dokümantasyon\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Kapalı\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Kapalı ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"İndir\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Süre\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Düzenle\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"{foo} düzenle\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"E-posta\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"E-posta bildirimleri\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Boş\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Bitiş Saati\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"Uç Nokta URL'si\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"Ping atılacak uç nokta URL'si (gerekli)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Şifreyi sıfırlamak için e-posta adresini girin\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"E-posta adresini girin...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Tek kullanımlık şifrenizi girin.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Geçici\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Hata\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Örnek:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Son {2, plural, one {# dakika} other {# dakika}} içinde {0}{1} aşıyor\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Çalıştırma ana PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"<0>config.yml</0> içinde tanımlanmayan mevcut sistemler silinecektir. Lütfen düzenli yedekler alın.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Aktif olarak çıktı\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Bir saat sonra veya hub yeniden başlatıldığında sona erer.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Dışa aktar\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Yapılandırmayı dışa aktar\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Mevcut sistem yapılandırmanızı dışa aktarın.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Fahrenhayt (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Başarısız\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Başarısız Özellikler:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Kimlik doğrulama başarısız\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Ayarlar kaydedilemedi\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Heartbeat gönderilemedi\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Test bildirimi gönderilemedi\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Uyarı güncellenemedi\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Başarısız: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Filtrele...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Parmak izi\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Donanım Yazılımı\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"<0>{min}</0> {min, plural, one {dakika} other {dakika}} için\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Şifrenizi mi unuttunuz?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD komutu\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Dolu\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Genel\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Genel\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU motorları\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU Güç Çekimi\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU Kullanımı\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Izgara\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Sağlık\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat İzleme\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat başarıyla gönderildi\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew komutu\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Ana Bilgisayar / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP Yöntemi\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP yöntemi: POST, GET veya HEAD (varsayılan: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Boşta\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Yönetici hesabınızın şifresini kaybettiyseniz, aşağıdaki komutu kullanarak sıfırlayabilirsiniz.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"İmaj\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Pasif\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Aralık\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Geçersiz e-posta adresi.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Dil\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Düzen\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Düzen genişliği\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Yaşam döngüsü\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"Sınır\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Yük Ortalaması\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Yük Ortalaması 15d\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Yük Ortalaması 1d\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Yük Ortalaması 5d\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Yük Ort.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Yükleme durumu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Yükleniyor...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Çıkış Yap\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Giriş Yap\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Giriş denemesi başarısız\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Günlükler\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Uyarı oluşturma yerini mi arıyorsunuz? Sistemler tablosundaki zil <0/> simgelerine tıklayın.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Ana PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Görüntüleme ve bildirim tercihlerini yönetin.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Manuel kurulum talimatları\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Maks 1 dk\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Bellek\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Bellek sınırı\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Bellek Tepe Değeri\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Bellek Kullanımı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker konteynerlerinin bellek kullanımı\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Model\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Ad\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Ağ\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker konteynerlerinin ağ trafiği\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Genel arayüzlerin ağ trafiği\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Ağ birimi\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Hayır\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Sonuç bulunamadı.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Sonuç yok.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Bu cihaz için kullanılabilir S.M.A.R.T. özelliği yok.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Sistem bulunamadı.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Bildirimler\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"OAuth 2 / OIDC desteği\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Her yeniden başlatmada, veritabanındaki sistemler dosyada tanımlanan sistemlerle eşleşecek şekilde güncellenecektir.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Bir seferlik\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Tek kullanımlık şifre\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Menüyü aç\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Veya devam et\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Diğer\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Mevcut uyarıların üzerine yaz\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Sayfa\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Sayfa {0} / {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Sayfalar / Ayarlar\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Şifre\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Şifre en az 8 karakter olmalıdır.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Parola 72 bayttan küçük olmalıdır.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Şifre sıfırlama isteği alındı\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Geçmiş\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Duraklat\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Duraklatıldı\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Duraklatıldı ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Yük (Payload) formatı\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Çekirdek başına ortalama kullanım\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Her durumda harcanan zamanın yüzdesi\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Kalıcı\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Kalıcılık\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Uyarıların teslim edilmesini sağlamak için lütfen bir SMTP sunucusu <0>yapılandırın</0>.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Daha fazla ayrıntı için lütfen günlükleri kontrol edin.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Lütfen kimlik bilgilerinizi kontrol edin ve tekrar deneyin\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Lütfen bir yönetici hesabı oluşturun\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Lütfen bu site için açılır pencereleri etkinleştirin\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Lütfen tekrar giriş yapın\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Talimatlar için lütfen <0>dokümantasyonu</0> inceleyin.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Lütfen hesabınıza giriş yapın\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Güç Açık\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Kayıtlı zamanda kesin kullanım\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Tercih Edilen Dil\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Süreç başlatıldı\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Genel Anahtar\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Sessiz Saatler\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Oku\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Alındı\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Yenile\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"İlişkiler\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Tek kullanımlık şifre iste\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"OTP iste\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Gerektiren\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Gerektirir\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Şifreyi Sıfırla\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Çözüldü\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Yeniden başlatmalar\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Devam et\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Kök\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Token'ı döndür\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Sayfa başına satır\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Çalışma Zamanı Metrikleri\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. Ayrıntıları\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. Kendiliğinden Test\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Adresleri enter tuşu veya virgül ile kaydedin. E-posta bildirimlerini devre dışı bırakmak için boş bırakın.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Ayarları Kaydet\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Sistemi kaydet\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Veritabanında kaydedilir ve siz devre dışı bırakana kadar süresi dolmaz.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Zamanla\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Bildirimlerin gönderilmeyeceği sessiz saatleri zamanlayın, örneğin bakım dönemleri sırasında.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Bildirimlerin gönderilmeyeceği sessiz saatleri zamanlayın.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Ara\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Sistemler veya ayarlar için ara...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Pingler arasındaki saniye (varsayılan: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Uyarıları nasıl alacağınızı yapılandırmak için <0>bildirim ayarlarını</0> inceleyin.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Seç {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Uç noktanızın çalıştığını doğrulamak için tek bir heartbeat ping'i gönderin.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Beszel'i internete maruz bırakmadan izleyebilmeniz için harici bir izleme servisine periyodik giden pingler gönderin.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Test heartbeat gönder\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Gönderildi\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Seri Numarası\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Hizmet Ayrıntıları\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Hizmetler\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Sayaç renkleri için yüzde eşiklerini ayarlayın.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Heartbeat izlemeyi etkinleştirmek için Beszel hub'ınızda aşağıdaki ortam değişkenlerini ayarlayın:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Ayarlar\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Ayarlar kaydedildi\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Giriş yap\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP ayarları\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sıralama Ölçütü\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Başlangıç Saati\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Durum\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Durum\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Alt Durum\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Sistem tarafından kullanılan takas alanı\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Takas Kullanımı\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Sistem\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Zaman içindeki sistem yükü ortalamaları\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd Hizmetleri\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Sistemler\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Sistemler, veri dizininizdeki bir <0>config.yml</0> dosyasında yönetilebilir.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Tablo\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Görevler\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Sıc\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Sıcaklık\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Sıcaklık birimi\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Sistem sensörlerinin sıcaklıkları\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"<0>URL</0>'yi Test Et\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Test heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Test bildirimi gönderildi\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Tüm sistemler çalışırken genel durum <0>ok</0>, uyarılar tetiklendiğinde <1>uyarı</1> ve herhangi bir sistem çöktüğünde <2>hata</2> şeklindedir.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Ardından arka uca giriş yapın ve kullanıcılar tablosunda kullanıcı hesabı şifrenizi sıfırlayın.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Bu işlem geri alınamaz. Bu, veritabanından {name} için tüm mevcut kayıtları kalıcı olarak silecektir.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Bu, seçilen tüm kayıtları veritabanından kalıcı olarak silecektir.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName} verimliliği\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Kök dosya sisteminin verimliliği\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Zaman formatı\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"E-posta(lar)a\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Izgarayı değiştir\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Temayı değiştir\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Token'lar ve Parmak İzleri\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Token'lar agentların bağlanıp kaydolmasına izin verir. Parmak izleri her sisteme özgü kararlı tanımlayıcılardır ve ilk bağlantıda ayarlanır.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Token'lar ve parmak izleri hub'a WebSocket bağlantılarını doğrulamak için kullanılır.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Toplam\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Her arayüz için alınan toplam veri\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Her arayüz için gönderilen toplam veri\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Toplam: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Tetikleyen\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Tetikleyiciler\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"1 dakikalık yük ortalaması bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"15 dakikalık yük ortalaması bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"5 dakikalık yük ortalaması bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Herhangi bir sensör bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Pil şarjı bir eşiğin altına düştüğünde tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Birleştirilmiş yukarı/aşağı bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"CPU kullanımı bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"GPU kullanımı bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Bellek kullanımı bir eşiği aştığında tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Durum yukarı ve aşağı arasında değiştiğinde tetiklenir\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Herhangi bir diskin kullanımı bir eşiği aştığında tetiklenir\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Tür\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Birim dosyası\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Birim tercihleri\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Evrensel token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Bilinmiyor\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Sınırsız\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Açık\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Açık ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Güncelle\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Güncellendi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Her 10 dakikada bir güncellenir.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Yükle\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Kullanım\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Kök bölümün kullanımı\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Kullanıldı\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Kullanıcılar\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Değer\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Görüntüle\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Daha fazla göster\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"En son 200 uyarınızı görüntüleyin.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Görünür Alanlar\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Görüntülemek için yeterli kayıt bekleniyor\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Çevirilerimizi daha iyi hale getirmemize yardımcı olmak ister misiniz? Daha fazla bilgi için <0>Crowdin</0> inceleyin.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"İstekler\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Uyarı (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Uyarı eşikleri\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Anlık bildirimler\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Etkinleştirildiğinde, bu token aracıların önceden sistem oluşturmadan kendilerini kaydetmelerine izin verir.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"POST kullanırken, her heartbeat sistem durumu özeti, çöken sistemlerin listesi ve tetiklenen uyarıları içeren bir JSON yükü içerir.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows komutu\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Yaz\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML Yapılandırması\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML Yapılandırması\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Evet\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Kullanıcı ayarlarınız güncellendi.\"\n"
  },
  {
    "path": "internal/site/src/locales/uk/uk.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: uk\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Ukrainian\\n\"\n\"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: uk\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"Вибрано {0} з {1} рядків.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# ядро} few {# ядра} many {# ядер} other {# ядер}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} день} few {{countString} дні} many {{countString} днів} other {{countString} дня}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} година} few {{countString} години} many {{countString} годин} other {{countString} години}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} хвилина} few {{countString} хвилини} many {{countString} хвилин} other {{countString} хвилини}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# потік} few {# потоки} many {# потоків} other {# потоків}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 година\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 хв\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 хвилина\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 тиждень\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 годин\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 хв\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 години\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 днів\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 хв\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Дії\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Активне\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Активні сповіщення\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Активний стан\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Додати {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Додати <0>Систему</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Додати систему\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Додати URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Налаштуйте параметри відображення для графіків.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Налаштувати ширину основного макету\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Адміністратор\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Після\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Після встановлення змінних оточення перезапустіть хаб Beszel, щоб зміни набули чинності.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Агент\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Історія сповіщень\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Сповіщення\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Всі контейнери\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Всі системи\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Ви впевнені, що хочете видалити {name}?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Ви впевнені?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Автоматичне копіювання вимагає безпечного контексту.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Середнє\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Середнє використання CPU контейнерами\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Середнє опускається нижче <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Середнє перевищує <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Середнє енергоспоживання GPUs\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Середнє використання CPU по всій системі\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Середнє використання {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Середнє використання рушіїв GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Резервні копії\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Пропускна здатність\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Bat\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Батарея\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Стало активним\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Стало неактивним\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"До\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Нижче {0}{1} протягом {2, plural, one {останньої # хвилини} other {останніх # хвилин}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel підтримує OpenID Connect та багато постачальників автентифікації OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel використовує <0>Shoutrrr</0> для інтеграції з популярними сервісами сповіщень.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Двійковий\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Біти (Кбіт/с, Мбіт/с, Гбіт/с)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Стан завантаження\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Байти (КБ/с, МБ/с, ГБ/с)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Кеш / Буфери\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Може перезавантажити\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Може запустити\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Може зупинити\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Скасувати\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Можливості\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Ємність\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Увага - можливе втрата даних\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Цельсій (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Змінити одиниці вимірювання для метрик.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Змінити загальні параметри програми.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Заряд\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Заряджається\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Параметри графіків\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Перевірте {email} для отримання посилання на скидання.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Перевірте журнали для отримання додаткової інформації.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Перевірте ваш сервіс моніторингу\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Перевірте свій сервіс сповіщень\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Очистити\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Натисніть на контейнер, щоб переглянути більше інформації.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Натисніть на пристрій, щоб переглянути більше інформації.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Натисніть на систему, щоб переглянути більше інформації.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Натисніть, щоб скопіювати\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Інструкції командного рядка\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Налаштуйте, як ви отримуєте сповіщення про тривоги.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Підтвердьте пароль\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Конфлікти\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"З'єднання розірвано\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Продовжити\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Скопійовано в буфер обміну\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Копіювати docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Копіювати docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Копіювати env\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Копіювати хост\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Копіювати команду Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Копіювати імʼя\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Копіювати текст\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Скопіюйте команду встановлення для агента нижче, або зареєструйте агентів автоматично за допомогою <0>універсального токена</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Скопіюйте вміст <0>docker-compose.yml</0> для агента нижче, або зареєструйте агентів автоматично за допомогою <1>універсального токена</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Копіювати YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"ЦП\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Ядра ЦП\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Пік ЦП\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Час ЦП\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Розподіл часу ЦП\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Використання ЦП\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Створити\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Створити обліковий запис\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Створено\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Критично (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Кумулятивне завантаження\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Кумулятивне відвантаження\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Поточний стан\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Цикли\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Щодня\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Стандартний період часу\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Видалити\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Видалити відбиток\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Опис\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Деталі\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Пристрій\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Розряджається\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Диск\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Дисковий ввід/вивід\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Одиниця виміру диска\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Використання диска\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Використання диска {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Використання ЦП Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Використання пам'яті Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Мережевий ввід/вивід Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Документація\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Не працює\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Не працює ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Завантажити\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Тривалість\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Редагувати\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Редагувати {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Електронна пошта\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Сповіщення електронною поштою\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Порожня\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Час завершення\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL-адреса кінцевої точки\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL-адреса кінцевої точки для пінгу (обов'язково)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Введіть адресу електронної пошти для скидання пароля\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Введіть адресу електронної пошти...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Введіть ваш одноразовий пароль.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Ефемерний\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Помилка\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Приклад:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Перевищує {0}{1} протягом {2, plural, one {останньої # хвилини} other {останніх # хвилин}}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"Основний PID процесу\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Існуючі системи, не визначені в <0>config.yml</0>, будуть видалені. Будь ласка, робіть регулярні резервні копії.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Завершилося активно\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Закінчується через годину або при перезапуску хаба.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Експорт\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Експорт конфігурації\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Експортуйте поточну конфігурацію систем.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Фаренгейт (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Не вдалося\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Невдалі атрибути:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Не вдалося автентифікувати\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Не вдалося зберегти налаштування\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Не вдалося надіслати heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Не вдалося надіслати тестове сповіщення\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Не вдалося оновити сповіщення\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Невдало: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Фільтр...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Відбиток\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Прошивка\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Протягом <0>{min}</0> {min, plural, one {хвилини} other {хвилин}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Забули пароль?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Команда FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Повна\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Загальні\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Глобально\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Рушії GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Енергоспоживання GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Використання GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Сітка\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Здоров'я\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Моніторинг Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat успішно надіслано\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Команда Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Хост / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP-метод\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP-метод: POST, GET або HEAD (за замовчуванням: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Неактивна\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Якщо ви втратили пароль до свого адміністративного облікового запису, ви можете скинути його за допомогою наступної команди.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Образ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Неактивне\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Інтервал\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Неправильна адреса електронної пошти.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Мова\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Макет\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Ширина макету\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Життєвий цикл\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"обмеження\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Середнє навантаження\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Середнє навантаження за 15 хв\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Середнє навантаження за 1 хв\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Середнє навантаження за 5 хв\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Сер. навантаження\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Стан завантаження\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Завантаження...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Вийти\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Увійти\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Спроба входу не вдалася\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Журнали\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Шукаєте, де створити сповіщення? Натисніть на іконки дзвінка <0/> в таблиці систем.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"Основний PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Керуйте параметрами відображення та сповіщень.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Інструкції з ручного налаштування\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Макс 1 хв\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Пам'ять\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Обмеження пам'яті\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Пік пам'яті\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Використання пам'яті\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Використання пам'яті контейнерами Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Модель\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Ім'я\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Мережа\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Мережевий трафік контейнерів Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Мережевий трафік публічних інтерфейсів\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Одиниця виміру мережі\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Ні\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Результатів не знайдено.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Немає результатів.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Для цього пристрою немає доступних атрибутів S.M.A.R.T.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Систем не знайдено.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Сповіщення\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Підтримка OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"При кожному перезапуску системи в базі даних будуть оновлені, щоб відповідати системам, визначеним у файлі.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Одноразовий\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Одноразовий пароль\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Відкрити меню\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Або продовжити з\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Інше\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Перезаписати існуючі сповіщення\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Сторінка\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Сторінка {0} з {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Сторінки / Налаштування\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Пароль\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Пароль має містити щонайменше 8 символів.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Пароль не повинен перевищувати 72 байти.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Запит на скидання пароля отримано\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Минуле\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Призупинити\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Призупинено\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Призупинено ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Формат корисного навантаження\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Середнє використання на ядро\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Відсоток часу, проведеного в кожному стані\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Постійний\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Стійкість\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Будь ласка, <0>налаштуйте SMTP сервер</0>, щоб забезпечити доставку сповіщень.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Будь ласка, перевірте журнали для отримання додаткової інформації.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Будь ласка, перевірте свої облікові дані та спробуйте ще раз\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Будь ласка, створіть адміністративний обліковий запис\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Будь ласка, увімкніть спливаючі вікна для цього сайту\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Будь ласка, увійдіть знову\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Будь ласка, перегляньте <0>документацію</0> для отримання інструкцій.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Будь ласка, увійдіть у свій обліковий запис\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Порт\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Увімкнення живлення\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Точне використання в записаний час\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Бажана мова\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Процес запущено\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Ключ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Тихі години\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Читання\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Отримано\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Оновити\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Зв'язки\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Запит одноразового пароля\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Запит OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Потрібно для\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Потребує\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Скинути пароль\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Вирішено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Перезапуски\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Відновити\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Корінь\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Оновити токен\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Рядків на сторінку\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Метрики виконання\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Деталі S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Самодіагностика S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Збережіть адресу, використовуючи клавішу Enter або кому. Залиште порожнім, щоб вимкнути сповіщення електронною поштою.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Зберегти налаштування\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Зберегти систему\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Збережено в базі даних і не закінчується, поки ви його не вимкнете.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Розклад\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Заплануйте години спокою, коли сповіщення не надсилатимуться, наприклад, під час періодів технічного обслуговування.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Заплануйте години спокою, коли сповіщення не надсилатимуться.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Пошук\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Шукати системи або налаштування...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Секунди між пінгами (за замовчуванням: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Перегляньте <0>налаштування сповіщень</0>, щоб налаштувати, як ви отримуєте сповіщення.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Вибрати {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Надішліть одиночний пінг heartbeat, щоб перевірити працездатність кінцевої точки.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Надсилайте періодичні вихідні пінги на зовнішній сервіс моніторингу, щоб ви могли моніторити Beszel без його публікації в інтернеті.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Надіслати тестовий heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Відправлено\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Серійний номер\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Деталі служби\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Служби\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Встановіть відсоткові пороги для кольорів лічильників.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Встановіть наступні змінні оточення в хабі Beszel для увімкнення моніторингу heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Налаштування\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Налаштування збережено\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Увійти\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Налаштування SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Сортувати за\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Час початку\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Стан\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Статус\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Підстан\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Область підкачки, використана системою\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Використання підкачки\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Система\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Середнє навантаження системи з часом\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Служби Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Системи\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Системи можуть керуватися у файлі <0>config.yml</0> у вашій директорії даних.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Таблиця\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Завдання\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Температура\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Температура\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Одиниця вимірювання температури\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Температури датчиків системи\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Тест <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Тестовий heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Тестове сповіщення надіслано\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Загальний статус: <0>ok</0>, коли всі системи працюють, <1>попередження</1>, коли спрацювали сповіщення, і <2>помилка</2>, коли будь-яка система відключена.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Потім увійдіть у бекенд і скиньте пароль вашого облікового запису користувача в таблиці користувачів.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Цю дію не можна скасувати. Це назавжди видалить всі поточні записи для {name} з бази даних.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Це назавжди видалить усі вибрані записи з бази даних.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Пропускна здатність {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Пропускна здатність кореневої файлової системи\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Формат часу\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"На електронну пошту\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Перемкнути сітку\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Перемкнути тему\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Токен\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Токени та Відбитки\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Токени дозволяють агентам підключатися та реєструватися. Відбитки - це стабільні ідентифікатори, унікальні для кожної системи, встановлюються при першому підключенні.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Токени та відбитки використовуються для автентифікації WebSocket з'єднань до хабу.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Разом\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Загальний обсяг отриманих даних для кожного інтерфейсу\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Загальний обсяг відправлених даних для кожного інтерфейсу\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Всього: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Запущено через\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Тригери\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Спрацьовує, коли середнє навантаження за 1 хвилину перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Спрацьовує, коли середнє навантаження за 15 хвилин перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Спрацьовує, коли середнє навантаження за 5 хвилин перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Спрацьовує, коли будь-який датчик перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Спрацьовує, коли заряд батареї опускається нижче порогу\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Спрацьовує, коли відправлення/отримання сумарно перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Спрацьовує, коли використання ЦП перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Спрацьовує, коли використання GPU перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Спрацьовує, коли використання пам'яті перевищує поріг\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Спрацьовує, коли статус перемикається між «працює» та «не працює»\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Спрацьовує, коли використання будь-якого диска перевищує поріг\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Тип\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Файл юніта\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Налаштування одиниць вимірювання\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Універсальний токен\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Невідома\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Необмежено\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Працює\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Працює ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Оновити\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Оновлено\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Оновлюється кожні 10 хвилин.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Відвантажити\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Використання\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Використання кореневого розділу\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Використано\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Користувачі\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Значення\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Вигляд\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Переглянути більше\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Переглянути 200 останніх сповіщень.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Видимі стовпці\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Очікування достатньої кількості записів для відображення\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Хочете допомогти покращити наші переклади? Подробиці на <0>Crowdin</0>.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Потребує\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Попередження (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Пороги попередження\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / Push сповіщення\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"При ввімкненні цей токен дозволяє агентам самостійно реєструватися без попереднього створення системи.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"При використанні POST кожен heartbeat містить корисне навантаження JSON із коротким звітом про стан системи, списком відключених систем та сповіщеннями, що спрацювали.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Команда Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Запис\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Конфігурація YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Конфігурація YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Так\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Ваші налаштування користувача були оновлені.\"\n"
  },
  {
    "path": "internal/site/src/locales/vi/vi.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: vi\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Vietnamese\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: vi\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"Đã chọn {0} trên {1} hàng.\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# nhân} other {# nhân}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} ngày} other {{countString} ngày}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} giờ} other {{countString} giờ}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} phút} few {{countString} phút} many {{countString} phút} other {{countString} phút}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# luồng} other {# luồng}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 giờ\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 phút\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 phút\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 tuần\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 giờ\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 phút\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 giờ\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 ngày\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 phút\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"Hành động\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"Hoạt động\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"Cảnh báo hoạt động\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"Trạng thái hoạt động\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"Thêm {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"Thêm <0>Hệ thống</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"Thêm hệ thống\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"Thêm URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"Điều chỉnh tùy chọn hiển thị cho biểu đồ.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"Điều chỉnh chiều rộng bố cục chính\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"Quản trị viên\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"Sau\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"Sau khi thiết lập các biến môi trường, hãy khởi động lại hub Beszel để các thay đổi có hiệu lực.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Tác nhân\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"Lịch sử Cảnh báo\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"Cảnh báo\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"Tất cả container\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"Tất cả Hệ thống\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"Bạn có chắc chắn muốn xóa {name} không?\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"Bạn có chắc không?\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"Sao chép tự động yêu cầu một ngữ cảnh an toàn.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"Trung bình\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"Sử dụng CPU trung bình của các container\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"Trung bình giảm xuống dưới <0>{value}{0}</0>\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"Trung bình vượt quá <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"Tiêu thụ điện năng trung bình của GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"Sử dụng CPU trung bình toàn hệ thống\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"Mức sử dụng trung bình của {0}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"Mức sử dụng trung bình của động cơ GPU\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"Sao lưu\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"Băng thông\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"Pin\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"Pin\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"Đã trở thành hoạt động\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"Đã trở thành không hoạt động\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"Trước\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Dưới {0}{1} trong {2, plural, one {# phút} other {# phút}} qua\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel hỗ trợ OpenID Connect và nhiều nhà cung cấp xác thực OAuth2.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel sử dụng <0>Shoutrrr</0> để tích hợp với các dịch vụ thông báo phổ biến.\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"Nhị phân\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"Bit (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"Trạng thái khởi động\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"Byte (KB/giây, MB/giây, GB/giây)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"Bộ nhớ đệm / Bộ đệm\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"Có thể tải lại\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"Có thể khởi động\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"Có thể dừng\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"Hủy bỏ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"Khả năng\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"Dung lượng\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"Cẩn thận - có thể mất dữ liệu\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"Độ C (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"Thay đổi đơn vị hiển thị cho các chỉ số.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"Thay đổi các tùy chọn ứng dụng chung.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"Sạc\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"Đang sạc\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"Tùy chọn biểu đồ\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"Kiểm tra {email} để lấy liên kết đặt lại.\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"Kiểm tra nhật ký để biết thêm chi tiết.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"Kiểm tra dịch vụ giám sát của bạn\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"Kiểm tra dịch vụ thông báo của bạn\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"Xóa\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"Nhấp vào container để xem thêm thông tin.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"Nhấp vào thiết bị để xem thêm thông tin.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"Nhấp vào hệ thống để xem thêm thông tin.\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"Nhấp để sao chép\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"Hướng dẫn dòng lệnh\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"Cấu hình cách bạn nhận thông báo cảnh báo.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"Xác nhận mật khẩu\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"Xung đột\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"Mất kết nối\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"Tiếp tục\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"Đã sao chép vào clipboard\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"Sao chép docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"Sao chép docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"Sao chép môi trường\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"Sao chép máy chủ\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"Sao chép lệnh Linux\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"Sao chép tên\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"Sao chép văn bản\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"Sao chép lệnh cài đặt cho tác nhân bên dưới hoặc tự động đăng ký tác nhân bằng <0>token chung</0>.\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"Sao chép nội dung <0>docker-compose.yml</0> cho tác nhân bên dưới hoặc tự động đăng ký tác nhân bằng <1>token chung</1>.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"Sao chép YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"Nhân CPU\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"Đỉnh CPU\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"Thời gian CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"Phân tích thời gian CPU\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"Sử dụng CPU\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"Tạo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"Tạo tài khoản\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"Đã tạo\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"Độ nghiêm trọng (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"Tải xuống tích lũy\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"Tải lên tích lũy\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"Trạng thái hiện tại\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"Chu kỳ\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"Hàng ngày\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"Thời gian mặc định\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"Xóa\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"Xóa vân tay\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"Mô tả\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"Chi tiết\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"Thiết bị\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"Đang xả\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"Đĩa\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"Đĩa I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"Đơn vị đĩa\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"Sử dụng Đĩa\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"Sử dụng đĩa của {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Sử dụng CPU Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Sử dụng Bộ nhớ Docker\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Mạng I/O Docker\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"Tài liệu\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"Mất kết nối\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"Mất kết nối ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"Tải xuống\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"Thời lượng\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"Chỉnh sửa\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"Chỉnh sửa {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"Email\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"Thông báo email\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"Hết pin\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"Thời gian kết thúc\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"URL điểm cuối\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"URL điểm cuối để ping (bắt buộc)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"Nhập địa chỉ email để đặt lại mật khẩu\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"Nhập địa chỉ email...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"Nhập mật khẩu một lần của bạn.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"Tạm thời\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"Lỗi\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"Ví dụ:\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"Vượt quá {0}{1} trong {2, plural, one {# phút} other {# phút}} qua\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"PID chính của Exec\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"Các hệ thống hiện có không được định nghĩa trong <0>config.yml</0> sẽ bị xóa. Vui lòng sao lưu thường xuyên.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"Đã thoát khi hoạt động\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"Hết hạn sau một giờ hoặc khi khởi động lại hub.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"Xuất\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"Xuất cấu hình\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"Xuất cấu hình hệ thống hiện tại của bạn.\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"Độ F (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"Thất bại\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"Thuộc tính thất bại:\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"Xác thực thất bại\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"Lưu cài đặt thất bại\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"Gửi heartbeat thất bại\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"Gửi thông báo thử nghiệm thất bại\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"Cập nhật cảnh báo thất bại\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"Thất bại: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"Lọc...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Vân tay\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"Phần sụn\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"Trong <0>{min}</0> {min, plural, one {phút} other {phút}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"Quên mật khẩu?\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"Lệnh FreeBSD\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"Đầy pin\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"Chung\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"Toàn cầu\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"Động cơ GPU\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"Mức tiêu thụ điện của GPU\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"Sử dụng GPU\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"Lưới\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"Sức khỏe\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Giám sát Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat đã được gửi thành công\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Lệnh Homebrew\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Máy chủ / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"Phương thức HTTP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"Phương thức HTTP: POST, GET hoặc HEAD (mặc định: POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"Không hoạt động\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"Nếu bạn đã mất mật khẩu cho tài khoản quản trị viên của mình, bạn có thể đặt lại bằng cách sử dụng lệnh sau.\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"Hình ảnh\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"Không hoạt động\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"Khoảng thời gian\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"Địa chỉ email không hợp lệ.\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"Ngôn ngữ\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"Bố cục\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"Chiều rộng bố cục\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"Vòng đời\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"giới hạn\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"Tải trung bình\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"Tải trung bình 15 phút\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"Tải trung bình 1 phút\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"Tải trung bình 5 phút\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"Tải TB\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"Trạng thái tải\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"Đang tải...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"Đăng xuất\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"Đăng nhập\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"Nỗ lực đăng nhập thất bại\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"Nhật ký\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"Thay vào đó, bạn đang tìm nơi để tạo cảnh báo? Nhấp vào biểu tượng chuông <0/> trong bảng hệ thống.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"PID chính\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"Quản lý tùy chọn hiển thị và thông báo.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"Hướng dẫn cài đặt thủ công\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"Tối đa 1 phút\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"Bộ nhớ\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"Giới hạn bộ nhớ\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"Đỉnh bộ nhớ\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"Sử dụng Bộ nhớ\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Sử dụng bộ nhớ của các container Docker\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"Mô hình\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"Tên\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"Mạng\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Lưu lượng mạng của các container Docker\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"Lưu lượng mạng của các giao diện công cộng\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"Đơn vị mạng\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"Không\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"Không tìm thấy kết quả.\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"Không có kết quả.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"Không có thuộc tính S.M.A.R.T. nào khả dụng cho thiết bị này.\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"Không tìm thấy hệ thống.\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"Thông báo\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"Hỗ trợ OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"Mỗi khi khởi động lại, các hệ thống trong cơ sở dữ liệu sẽ được cập nhật để khớp với các hệ thống được định nghĩa trong tệp.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"Một lần\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"Mật khẩu một lần\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"Mở menu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"Hoặc tiếp tục với\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"Khác\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"Ghi đè các cảnh báo hiện có\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"Trang\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"Trang {0} trên {1}\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"Trang / Cài đặt\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"Mật khẩu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"Mật khẩu phải có ít nhất 8 ký tự.\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"Mật khẩu phải nhỏ hơn 72 byte.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"Yêu cầu đặt lại mật khẩu đã được nhận\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"Quá khứ\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"Tạm dừng\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"Đã tạm dừng\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"Đã tạm dừng ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"Định dạng tải trọng (payload)\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"Tỷ lệ sử dụng trung bình mỗi nhân\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"Phần trăm thời gian dành cho mỗi trạng thái\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"Vĩnh viễn\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"Tính bền vững\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"Vui lòng <0>cấu hình máy chủ SMTP</0> để đảm bảo cảnh báo được gửi đi.\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"Vui lòng kiểm tra nhật ký để biết thêm chi tiết.\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"Vui lòng kiểm tra thông tin đăng nhập của bạn và thử lại\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"Vui lòng tạo một tài khoản quản trị viên\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"Vui lòng bật cửa sổ bật lên cho trang web này\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"Vui lòng đăng nhập lại\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"Vui lòng xem <0>tài liệu</0> để biết hướng dẫn.\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"Vui lòng đăng nhập vào tài khoản của bạn\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Cổng\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"Bật nguồn\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"Sử dụng chính xác tại thời điểm ghi nhận\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"Ngôn ngữ Ưa thích\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"Tiến trình đã khởi động\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"Khóa\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"Giờ yên tĩnh\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"Đọc\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"Đã nhận\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"Làm mới\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"Mối quan hệ\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"Yêu cầu mật khẩu một lần\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"Yêu cầu OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"Được yêu cầu bởi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"Yêu cầu\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"Đặt lại Mật khẩu\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"Đã giải quyết\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"Khởi động lại\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"Tiếp tục\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Gốc\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"Xoay vòng token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"Số hàng mỗi trang\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"Chỉ số thời gian chạy\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"Chi tiết S.M.A.R.T.\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"Tự kiểm tra S.M.A.R.T.\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"Lưu địa chỉ bằng cách sử dụng phím enter hoặc dấu phẩy. Để trống để vô hiệu hóa thông báo email.\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"Lưu Cài đặt\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"Lưu hệ thống\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"Được lưu trong cơ sở dữ liệu và không hết hạn cho đến khi bạn tắt nó.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"Lịch trình\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"Lên lịch giờ yên tĩnh nơi thông báo sẽ không được gửi, chẳng hạn như trong thời gian bảo trì.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"Lên lịch giờ yên tĩnh nơi thông báo sẽ không được gửi.\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"Tìm kiếm\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"Tìm kiếm hệ thống hoặc cài đặt...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Số giây giữa các lần ping (mặc định: 60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"Xem <0>cài đặt thông báo</0> để cấu hình cách bạn nhận cảnh báo.\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"Chọn {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"Gửi một ping heartbeat duy nhất để xác minh điểm cuối của bạn đang hoạt động.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"Gửi các ping gửi đi định kỳ đến dịch vụ giám sát bên ngoài để bạn có thể giám sát Beszel mà không cần để lộ nó với internet.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"Gửi heartbeat thử nghiệm\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"Đã gửi\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"Số seri\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"Chi tiết dịch vụ\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"Dịch vụ\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"Đặt ngưỡng cho màu sắc đồng hồ.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"Thiết lập các biến môi trường sau trên hub Beszel của bạn để bật giám sát heartbeat:\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"Cài đặt\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"Cài đặt đã được lưu\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"Đăng nhập\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"Cài đặt SMTP\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"Sắp xếp theo\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"Thời gian bắt đầu\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"Trạng thái\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"Trạng thái\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"Trạng thái phụ\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"Không gian hoán đổi được sử dụng bởi hệ thống\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"Sử dụng Hoán đổi\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"Hệ thống\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"Tải trung bình của hệ thống theo thời gian\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Dịch vụ Systemd\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"Các hệ thống\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"Các hệ thống có thể được quản lý trong tệp <0>config.yml</0> bên trong thư mục dữ liệu của bạn.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"Bảng\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"Tác vụ\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"Nhiệt độ\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"Nhiệt độ\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"Đơn vị nhiệt độ\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"Nhiệt độ của các cảm biến hệ thống\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"Kiểm tra <0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"Thử nghiệm heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"Thông báo thử nghiệm đã được gửi\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"Trạng thái tổng thể là <0>ok</0> khi tất cả các hệ thống đều hoạt động, <1>cảnh báo</1> khi các cảnh báo được kích hoạt và <2>lỗi</2> khi có bất kỳ hệ thống nào bị hỏng.\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"Sau đó đăng nhập vào backend và đặt lại mật khẩu tài khoản người dùng của bạn trong bảng người dùng.\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"Hành động này không thể hoàn tác. Điều này sẽ xóa vĩnh viễn tất cả các bản ghi hiện tại cho {name} khỏi cơ sở dữ liệu.\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"Thao tác này sẽ xóa vĩnh viễn tất cả các bản ghi đã chọn khỏi cơ sở dữ liệu.\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"Thông lượng của {extraFsName}\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Thông lượng của hệ thống tệp gốc\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"Định dạng thời gian\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"Đến email(s)\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"Chuyển đổi lưới\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"Chuyển đổi chủ đề\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Token & Vân tay\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Token cho phép các tác nhân kết nối và đăng ký. Vân tay là các định danh ổn định duy nhất cho mỗi hệ thống, được đặt khi kết nối lần đầu.\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Token và vân tay được sử dụng để xác thực các kết nối WebSocket đến trung tâm.\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"Tổng\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"Tổng dữ liệu nhận được cho mỗi giao diện\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"Tổng dữ liệu gửi đi cho mỗi giao diện\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"Tổng: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"Được kích hoạt bởi\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"Kích hoạt\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"Kích hoạt khi tải trung bình 1 phút vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"Kích hoạt khi tải trung bình 15 phút vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"Kích hoạt khi tải trung bình 5 phút vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"Kích hoạt khi bất kỳ cảm biến nào vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"Kích hoạt khi mức pin giảm xuống dưới ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"Kích hoạt khi kết hợp lên/xuống vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"Kích hoạt khi sử dụng CPU vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"Kích hoạt khi sử dụng GPU vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"Kích hoạt khi sử dụng bộ nhớ vượt quá ngưỡng\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"Kích hoạt khi trạng thái chuyển đổi giữa lên và xuống\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"Kích hoạt khi sử dụng bất kỳ đĩa nào vượt quá ngưỡng\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"Loại\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"Tệp đơn vị\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"Tùy chọn đơn vị\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"Token chung\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"Không xác định\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"Không giới hạn\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"Hoạt động\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"Hoạt động ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"Cập nhật\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"Đã cập nhật\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"Cập nhật mỗi 10 phút.\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"Tải lên\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"Uptime\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"Sử dụng\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Sử dụng phân vùng gốc\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"Đã sử dụng\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"Người dùng\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"Giá trị\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"Xem\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"Xem thêm\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"Xem 200 cảnh báo gần đây nhất của bạn.\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"Các cột hiển thị\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"Đang chờ đủ bản ghi để hiển thị\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"Muốn giúp chúng tôi cải thiện bản dịch của mình? Xem <0>Crowdin</0> để biết thêm chi tiết.\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"Muốn\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"Cảnh báo (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"Ngưỡng cảnh báo\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Thông báo Webhook / Push\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"Khi được bật, token này cho phép các tác nhân tự đăng ký mà không cần tạo hệ thống trước.\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"Khi sử dụng POST, mỗi heartbeat bao gồm một tải trọng JSON với bản tóm tắt trạng thái hệ thống, danh sách các hệ thống bị hỏng và các cảnh báo đã được kích hoạt.\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Lệnh Windows\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"Ghi\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"Cấu hình YAML\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"Cấu hình YAML\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"Có\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"Cài đặt người dùng của bạn đã được cập nhật.\"\n"
  },
  {
    "path": "internal/site/src/locales/zh/zh.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Chinese Traditional\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: zh-TW\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"已選取 {1} 個項目中的 {0} 個\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# 核心} other {# 核心}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} 天} other {{countString} 天}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} 小時} other {{countString} 小時}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} 分鐘} few {{countString} 分鐘} many {{countString} 分鐘} other {{countString} 分鐘}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# 執行緒} other {# 執行緒}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1小時\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1週\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12小時\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24小時\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30天\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 分鐘\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"操作\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"啟用中\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"活動警報\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"活動狀態\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"新增{foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"新增<0>系統</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"新增系統\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"新增 URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"調整圖表的顯示選項。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"調整主版面寬度\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"管理員\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"之後\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"設置環境變數後，重新啟動您的 Beszel hub 以使更改生效。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"Agent\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"警報歷史\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"警報\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"所有容器\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"所有系統\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"您確定要刪除 {name} 嗎？\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"您確定嗎？\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"只有在受保護的環境（HTTPS）才能自動複製。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"平均\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"容器的平均 CPU 使用率\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"平均值降至<0>{value}{0}</0>以下\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"平均值超過<0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU 的平均功耗\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"系統的平均 CPU 使用率\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0} 的平均使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU 引擎的平均利用率\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"備份\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"網路流量\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"電池\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"電池\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"啟用\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"停用\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"之前\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在過去的{2, plural, one {# 分鐘} other {# 分鐘}}中低於{0}{1}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel 支援 OpenID Connect 和許多 OAuth2 認證提供者。\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel 以 <0>Shoutrrr</0> 整合常用的通知服務。\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"執行檔\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"位元 (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"啟動狀態\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"位元組 (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"快取/緩衝\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"可重載\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"可啟動\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"可停止\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"能力\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"容量\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"注意 - 可能遺失資料\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"攝氏 (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"更改指標的顯示單位。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"修改一般應用程式選項。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"充電\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"充電中\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"圖表選項\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"檢查 {email} 以取得重設連結。\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"檢查系統記錄以取得更多資訊。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"檢查您的監控服務\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"檢查您的通知服務\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"點擊容器以查看更多資訊。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"點擊裝置以查看更多資訊。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"點擊系統以查看更多資訊。\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"點擊複製\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"命令列指令\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"設定您要如何接收警報通知\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"確認密碼\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"衝突\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"連線中斷\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"繼續\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"已複製到剪貼簿\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"複製 docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"複製 docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"複製環境變數\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"複製主機\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"複製 Linux 指令\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"複製名稱\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"複製文字\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"以下方指令安裝 Agent ，或使用<0>通用 Token</0>進行自動註冊。\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"以下方<0>docker-compose.yml</0>執行 Agent，或使用<1>通用 Token</1>進行自動註冊。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"複製 YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU 核心\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU 峰值\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU 時間\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU 時間細分\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU 使用率\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"建立\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"建立帳號\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"已建立\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"嚴重 (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"累計下載\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"累計上傳\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"目前狀態\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"循環\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"每日\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"預設時間段\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"刪除\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"刪除 Fingerprint\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"描述\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"詳細資訊\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"裝置\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"放電中\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"磁碟\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"磁碟 I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"磁碟單位\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"磁碟使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName}的磁碟使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU 使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker 記憶體使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker 網路 I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"文件\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"離線\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"離線 ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"下載\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"持續時間\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"編輯\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"編輯{foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"電子郵件\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"電子郵件通知\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"空電\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"結束時間\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"端點 URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"要 ping 的端點 URL (必填)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"輸入電子郵件地址以重設密碼\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"輸入電子郵件地址...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"輸入您的一次性密碼。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"臨時\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"錯誤\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"範例：\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在過去的{2, plural, one {# 分鐘} other {# 分鐘}}中超過{0}{1}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"執行主 PID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"未在 <0>config.yml</0> 中定義的現有系統將會被刪除。請定期備份。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"結束\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"在一個小時後或者重新啟動 Hub 時過期。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"匯出\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"匯出設定\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"匯出您現在的系統設定。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"華氏 (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"失敗\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"失敗屬性：\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"認證失敗\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"儲存設定失敗\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"發送 heartbeat 失敗\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"發送測試通知失敗\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"更新警報失敗\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"失敗：{0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"篩選...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"Fingerprint\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"韌體\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"忘記密碼？\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD 指令\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"滿電\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"一般\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"全域\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU 引擎\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU 功耗\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU 使用率\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"網格\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"健康狀態\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat 監控\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat 發送成功\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew 指令\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"Host / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP 方法\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP 方法：POST、GET 或 HEAD (預設：POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"閒置\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"如果您遺失管理員帳號密碼，可以使用以下指令重設。\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"映像檔\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"未啟用\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"間隔\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"無效的電子郵件地址。\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"語言\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"版面配置\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"版面寬度\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"生命週期\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"限制\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"15分鐘平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"1分鐘平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"5分鐘平均負載\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"平均負載\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"載入狀態\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"載入中...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"登出\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"登入\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"登入失敗\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"系統記錄\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"在尋找從哪裡建立警報嗎？點擊系統列表中的小鈴鐺<0/>。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"主 PID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"管理顯示和通知偏好。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"手動設定說明\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"最多 1 分鐘\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"記憶體\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"記憶體限制\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"記憶體峰值\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"記憶體使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker 容器的記憶體使用量\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"型號\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"名稱\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"網路\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker 容器的網路流量\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"公開介面的網路流量\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"網路單位\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"否\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"找不到結果。\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"沒有結果。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"此裝置沒有可用的 S.M.A.R.T. 屬性。\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"找不到任何系統。\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"通知\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"支援 OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"每次重新啟動時，將會以檔案中的系統定義更新資料庫。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"一次性\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"一次性密碼\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"開啟選單\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"或繼續使用\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"其他\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"覆蓋現有警報\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"頁面\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"第 {0} 頁，共 {1} 頁\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"頁面 / 設定\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"密碼\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"密碼需要至少 8 個字元。\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"密碼必須少於 72 個位元組。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"已收到密碼重設請求\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"已過期\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"暫停\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"已暫停\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"已暫停 ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"有效載荷 (Payload) 格式\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"核心平均使用率\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"狀態時間佔比\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"永久\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"持久性\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"請<0>設定一個 SMTP 伺服器</0>以確保能傳送警報。\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"請檢查系統記錄以取得更多資訊。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"請檢查您的憑證後重試\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"請建立一個管理員帳號\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"請為此網站啟用彈出視窗\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"請重新登入\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"請參閱<0>文件</0>以取得說明。\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"請登入您的帳號\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"Port\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"電源開啟\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"紀錄時間內的精確使用量\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"首選語言\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"進程啟動\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"公鑰\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"靜音時段\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"讀取\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"接收\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"重新整理\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"關係\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"請求一次性密碼\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"請求 OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"被依賴\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"依賴\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"重設密碼\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"已解決\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"重啟次數\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"繼續\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"Root\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"重設 Token\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"每頁列數\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"運行時指標\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. 詳細資訊\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. 自我測試\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"使用 Enter 鍵或逗號儲存地址。留空以停用電子郵件通知。\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"儲存設定\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"儲存系統\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"儲存在資料庫中，在您停用之前不會過期。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"排程\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"安排不會發送通知的靜音時段，例如在維護期間。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"安排不會發送通知的靜音時段。\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"搜尋\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"在設定或系統中搜尋...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Ping 之間的秒數 (預設：60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"查看<0>通知設定</0>以設定您如何接收警報。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"選取{foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"發送單個 heartbeat ping 以驗證您的端點是否正常工作。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"定期向外部監控服務發送出站 ping，以便您在不將 Beszel 暴露於網際網路的情況下進行監控。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"發送測試 heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"傳送\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"序號\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"服務詳細資訊\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"服務\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"設定儀表顏色的百分比閾值。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"在您的 Beszel hub 上設置以下環境變數以啟用 heartbeat 監控：\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"設定\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"已儲存設定\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"登入\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP 設定\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"排序\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"開始時間\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"狀態\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"狀態\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"子狀態\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"系統的虛擬記憶體使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"交換空間使用量\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"系統\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"系統平均負載隨時間變化\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd 服務\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"系統\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"可以用您Data資料夾中的<0>config.yml</0>來管理系統。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"列表\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"任務數\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"溫度\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"溫度\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"溫度單位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"系統感應器的溫度\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"測試<0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"測試 heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"已發送測試通知\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"當所有系統都正常運行時，整體狀態為 <0>ok</0>；當觸發警報時為 <1>警告</1>；當任何系統故障時為 <2>錯誤</2>。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"然後登入後台並在使用者列表中重設您的帳號密碼。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"此操作無法復原。這將永久刪除資料庫中{name}的所有當前記錄。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"這將從資料庫中永久刪除所有選定的記錄。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName}的傳輸速率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"Root 檔案系統的傳輸速率\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"時間格式\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"發送到電子郵件\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"切換網格\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"切換主題\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"Token\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"Token & Fingerprint\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"Token 用於 Agent 的連線和註冊。Fingerprint 則是每個系統唯一的穩定識別碼，於初次連線時設定。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"Token 與 Fingerprint 用於驗證連往 Hub 的 WebSocket 連線。\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"總計\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"每個介面的總接收資料量\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"每個介面的總傳送資料量\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"總計：{0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"觸發者\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"觸發器\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"當 1 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"當 15 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"當 5 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"當任何感應器超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"當電池電量降至閾值以下時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"當總流量超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"當 CPU 使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"當 GPU 使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"當記憶體使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"當連線和離線時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"當任何磁碟使用率超過閾值時觸發\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"類型\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"單元檔案\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"單位偏好\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"通用 Token\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"未知\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"無限制\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"上線\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"上線 ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"更新\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"已更新\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"每 10 分鐘更新一次。\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"上傳\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"運行時間\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"Root 分區的使用量\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"已使用\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"使用者\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"值\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"檢視\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"查看更多\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"檢視最近 200 則警報。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"顯示欄位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"等待足夠的記錄以顯示\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"想幫助我們改善翻譯嗎？查看<0>Crowdin</0>以取得更多詳細資訊。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"可選依賴\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"警告 (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"警告閾值\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / 推送通知\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"啟用後，此 Token 可讓 Agent 自行註冊，無需預先在系統中建立項目。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"使用 POST 時，每個 heartbeat 都包含一個 JSON 有效載荷，其中包含系統狀態摘要、故障系統列表和觸發的警報。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows 指令\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"寫入\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML 設定檔\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML 設定檔\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"是\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"已更新您的使用者設定\"\n"
  },
  {
    "path": "internal/site/src/locales/zh-CN/zh-CN.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Chinese Simplified\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: zh-CN\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"已选择 {0} / {1} 行\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# 核心} other {# 核心}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} 天} other {{countString} 天}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} 小时} other {{countString} 小时}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} 分钟} few {{countString} 分钟} many {{countString} 分钟} other {{countString} 分钟}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# 线程} other {# 线程}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1 小时\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 分钟\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 分钟\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1 周\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12 小时\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 分钟\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24 小时\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30 天\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 分钟\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"操作\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"活跃\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"启用的警报\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"活动状态\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"添加 {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"添加<0>客户端</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"添加客户端\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"添加 URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"调整图表的显示选项。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"调整主布局宽度\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"管理员\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"之后\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"设置环境变量后，重新启动您的 Beszel hub 以使更改生效。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"客户端\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"警报历史\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"警报\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"所有容器\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"所有客户端\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"您确定要删除 {name} 吗？\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"您确定吗？\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"自动复制所需的安全上下文。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"平均\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"容器的平均 CPU 使用率\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"平均值降至<0>{value}{0}</0>以下\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"平均值超过<0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU 平均能耗\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"系统范围内的平均 CPU 使用率\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0} 平均利用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU 引擎的平均利用率\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"备份\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"带宽\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"电池\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"电池\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"变为活动\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"变为非活动\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"之前\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在过去的{2, plural, one {# 分钟} other {# 分钟}}中低于{0}{1}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel 支持 OpenID Connect 和其他 OAuth2 认证方式。\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel 使用 <0>Shoutrrr</0> 以实现与常见的通知服务集成。\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"二进制\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"比特 (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"启动状态\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"字节 (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"缓存/缓冲区\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"可重载\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"可启动\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"可停止\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"能力\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"容量\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"注意 - 数据可能已经丢失\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"摄氏度 (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"更改指标的显示单位。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"更改常规应用程序选项。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"充电\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"充电中\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"图表选项\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"检查 {email} 以获取重置链接。\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"检查日志以获取更多详细信息。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"检查您的监控服务\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"检查您的通知服务\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"点击容器查看更多信息。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"点击设备查看更多信息。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"点击系统查看更多信息。\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"点击复制\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"命令行说明\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"配置您接收警报通知的方式。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"确认密码\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"冲突\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"连接已断开\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"继续\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"已复制到剪贴板\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"复制 docker compose 文件\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"复制 docker run 命令\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"复制环境变量\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"复制主机名\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"复制 Linux 安装命令\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"复制名称\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"复制文本\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"复制下面的客户端安装命令，或使用<0>通用令牌</0>自动注册客户端。\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"复制下面的客户端<0>docker-compose.yml</0>内容，或使用<1>通用令牌</1>自动注册客户端。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"复制 YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"CPU\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU 核心\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU 峰值\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU 时间\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU 时间细分\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU 使用率\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"创建\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"创建账户\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"创建时间\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"临界 (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"累计下载\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"累计上传\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"当前状态\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"循环次数\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"每日\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"默认时间段\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"删除\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"删除指纹\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"描述\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"详情\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"设备\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"放电中\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"磁盘\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"磁盘 I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"磁盘单位\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"磁盘使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName}的磁盘使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU 使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker 内存使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker 网络 I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"文档\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"离线\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"离线 ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"下载\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"持续时间\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"编辑\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"编辑 {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"电子邮件\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"电子邮件通知\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"空电\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"结束时间\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"端点 URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"要 ping 的端点 URL (必填)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"输入电子邮件地址以重置密码\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"输入电子邮件地址...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"输入您的一次性密码。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"临时\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"错误\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"示例：\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在过去的{2, plural, one {# 分钟} other {# 分钟}}中超过{0}{1}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"执行主进程 ID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"未在<0>config.yml</0>中定义的客户端将被删除。请定期备份。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"退出活动状态\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"一小时后或重新启动集线器时过期。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"导出\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"导出配置\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"导出您当前的系统配置。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"华氏度 (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"失败\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"失败属性：\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"认证失败\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"保存设置失败\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"发送 heartbeat 失败\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"发送测试通知失败\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"更新警报失败\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"失败: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"过滤...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"指纹\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"固件\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"持续<0>{min}</0> {min, plural, one {分钟} other {分钟}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"忘记密码？\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD 命令\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"满电\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"常规\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"全局\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU 引擎\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU 功耗\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU 使用率\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"网格\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"健康\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat 监控\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat 发送成功\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew 安装命令\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"主机/IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP 方法\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP 方法：POST、GET 或 HEAD (默认：POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"闲置\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"如果您丢失了管理员账户的密码，可以使用以下命令重置。\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"镜像\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"非活跃\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"间隔\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"无效的电子邮件地址。\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"语言\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"布局\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"布局宽度\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"生命周期\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"限制\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"系统负载\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"15 分钟内的平均负载\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"1 分钟负载平均值\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"5 分钟内的平均负载\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"负载\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"加载状态\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"加载中...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"登出\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"登录\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"登录尝试失败\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"日志\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"在寻找创建警报的位置吗？点击系统表中的铃铛<0/>图标。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"主进程 ID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"管理显示和通知偏好。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"手动设置说明\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"1分钟内最大值\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"内存\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"内存限制\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"内存峰值\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"内存使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker 容器的内存使用率\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"型号\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"名称\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"网络\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker 容器的网络流量\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"公共接口的网络流量\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"网络单位\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"否\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"未找到结果。\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"无结果。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"此设备没有可用的 S.M.A.R.T. 属性。\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"未找到系统。\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"通知\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"支持 OAuth 2/OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"每次重启时，数据库中的系统将更新以匹配文件中定义的系统。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"一次性\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"一次性密码\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"打开菜单\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"或使用以下方式登录\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"其他\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"覆盖现有警报\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"页面\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"第 {0} 页，共 {1} 页\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"页面 / 设置\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"密码\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"密码必须至少包含 8 个字符。\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"密码必须小于 72 字节。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"已收到密码重置请求\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"过去\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"暂停\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"已暂停\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"已暂停 ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"有效载荷 (Payload) 格式\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"每个核心的平均利用率\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"在每个状态下花费的时间百分比\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"永久\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"持久性\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"请<0>配置 SMTP 服务器</0>以确保警报被传递。\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"请检查日志以获取更多详细信息。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"请检查您的凭据并重试\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"请创建一个管理员账户\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"请为此网站启用弹出窗口\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"请重新登录\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"请参阅<0>文档</0>以获取说明。\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"请登录您的账户\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"端口\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"开机时间\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"采集时间下的精确内存使用率\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"首选语言\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"进程启动\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"公钥\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"静默时间\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"读取\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"接收\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"刷新\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"关系\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"请求一次性密码\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"请求 OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"被需要\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"需要\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"重置密码\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"已解决\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"重启次数\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"恢复\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"根\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"轮换令牌\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"每页行数\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"运行时指标\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. 详情\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. 自检\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"使用回车键或逗号保存地址。留空以禁用电子邮件通知。\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"保存设置\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"保存系统\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"保存在数据库中，在您禁用之前不会过期。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"计划\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"安排静默时间，在此期间不会发送通知，例如在维护期间。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"安排静默时间，在此期间不会发送通知。\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"搜索\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"搜索系统或设置...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Ping 之间的秒数 (默认：60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"查看<0>通知设置</0>以配置您接收警报的方式。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"选择 {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"发送单个 heartbeat ping 以验证您的端点是否正常工作。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"定期向外部监控服务发送出站 ping，以便您在不将 Beszel 暴露于互联网的情况下进行监控。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"发送测试 heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"发送\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"序列号\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"服务详情\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"服务\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"设置仪表颜色的百分比阈值。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"在您的 Beszel hub 上设置以下环境变量以启用 heartbeat 监控：\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"设置\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"设置已保存\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"登录\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP 设置\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"排序依据\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"开始时间\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"状态\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"状态\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"子状态\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"系统使用的 SWAP 空间\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"SWAP 使用率\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"系统\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"系统负载平均值随时间变化\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd 服务\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"系统\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"系统可以在数据目录中的<0>config.yml</0>文件中管理。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"表格\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"任务数\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"温度\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"温度\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"温度单位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"系统传感器的温度\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"测试<0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"测试 heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"测试通知已发送\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"当所有系统都正常运行时，整体状态为 <0>ok</0>；当触发警报时为 <1>警告</1>；当任何系统故障时为 <2>错误</2>。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"然后登录到后台并在用户表中重置您的用户账户密码。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"此操作无法撤销。这将永久删除数据库中{name}的所有当前记录。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"这将永久删除数据库中所有选定的记录。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName}的吞吐量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"根文件系统的吞吐量\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"时间格式\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"发送到电子邮件\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"切换网格\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"切换主题\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"令牌\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"令牌与指纹\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"令牌允许客户端连接和注册。指纹是每个系统唯一的稳定标识符，在首次连接时设置。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"令牌与指纹用于验证到中心的 WebSocket 连接。\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"总计\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"每个接口的总接收数据量\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"每个接口的总发送数据量\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"总计: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"由...触发\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"触发器\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"当 1 分钟负载平均值超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"当 15 分钟负载平均值超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"当 5 分钟内的平均负载超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"当任何传感器超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"当电池电量降至阈值以下时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"当网络的上/下行速度超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"当 CPU 使用率超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"当 GPU 使用率超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"当内存使用率超过阈值时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"当状态在上线与掉线之间切换时触发\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"当任何磁盘的使用率超过阈值时触发\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"类型\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"单元文件\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"单位偏好\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"通用令牌\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"未知\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"无限制\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"在线\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"在线 ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"更新\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"更新于\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"每 10 分钟更新一次。\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"上传\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"运行时间\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"根分区的使用\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"已用\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"用户\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"值\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"视图\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"查看更多\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"查看您最近的200个警报。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"可见列\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"正在收集足够的数据来显示\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"想帮助我们改进翻译吗？查看<0>Crowdin</0>以获取更多详细信息。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"希望\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"警告 (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"警告阈值\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / 推送通知\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"启用后，此令牌允许代理无需事先创建系统即可自行注册。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"使用 POST 时，每个 heartbeat 都包含一个 JSON 有效载荷，其中包括系统状态摘要、故障系统列表和触发的警报。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows 安装命令\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"写入\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML 配置\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML 配置\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"是\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"您的用户设置已更新。\"\n"
  },
  {
    "path": "internal/site/src/locales/zh-HK/zh-HK.po",
    "content": "msgid \"\"\nmsgstr \"\"\n\"POT-Creation-Date: 2024-11-01 11:30-0400\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"X-Generator: @lingui/cli\\n\"\n\"Language: zh\\n\"\n\"Project-Id-Version: beszel\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"PO-Revision-Date: 2026-01-31 21:16\\n\"\n\"Last-Translator: \\n\"\n\"Language-Team: Chinese Traditional, Hong Kong\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"X-Crowdin-Project: beszel\\n\"\n\"X-Crowdin-Project-ID: 733311\\n\"\n\"X-Crowdin-Language: zh-HK\\n\"\n\"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\\n\"\n\"X-Crowdin-File-ID: 32\\n\"\n\n#. placeholder {0}: table.getFilteredSelectedRowModel().rows.length\n#. placeholder {1}: table.getFilteredRowModel().rows.length\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"{0} of {1} row(s) selected.\"\nmsgstr \"已選擇 {1} 個項目中的 {0} 個\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{cores, plural, one {# core} other {# cores}}\"\nmsgstr \"{cores, plural, one {# 核心} other {# 核心}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} day} other {{countString} days}}\"\nmsgstr \"{count, plural, one {{countString} 天} other {{countString} 天}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} hour} other {{countString} hours}}\"\nmsgstr \"{count, plural, one {{countString} 小時} other {{countString} 小時}}\"\n\n#: src/lib/utils.ts\nmsgid \"{count, plural, one {{countString} minute} few {{countString} minutes} many {{countString} minutes} other {{countString} minutes}}\"\nmsgstr \"{count, plural, one {{countString} 分鐘} few {{countString} 分鐘} many {{countString} 分鐘} other {{countString} 分鐘}}\"\n\n#: src/components/routes/system/info-bar.tsx\nmsgid \"{threads, plural, one {# thread} other {# threads}}\"\nmsgstr \"{threads, plural, one {# 執行緒} other {# 執行緒}}\"\n\n#: src/lib/utils.ts\nmsgid \"1 hour\"\nmsgstr \"1小時\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"1 min\"\nmsgstr \"1 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"1 minute\"\nmsgstr \"1 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"1 week\"\nmsgstr \"1週\"\n\n#: src/lib/utils.ts\nmsgid \"12 hours\"\nmsgstr \"12小時\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"15 min\"\nmsgstr \"15 分鐘\"\n\n#: src/lib/utils.ts\nmsgid \"24 hours\"\nmsgstr \"24小時\"\n\n#: src/lib/utils.ts\nmsgid \"30 days\"\nmsgstr \"30天\"\n\n#. Load average\n#: src/components/charts/load-average-chart.tsx\nmsgid \"5 min\"\nmsgstr \"5 分鐘\"\n\n#. Table column\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Actions\"\nmsgstr \"操作\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Active\"\nmsgstr \"啟用中\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Active Alerts\"\nmsgstr \"活動警報\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Active state\"\nmsgstr \"活動狀態\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Add {foo}\"\nmsgstr \"新增 {foo}\"\n\n#: src/components/add-system.tsx\nmsgid \"Add <0>System</0>\"\nmsgstr \"新增<0>系統</0>\"\n\n#: src/components/add-system.tsx\nmsgid \"Add system\"\nmsgstr \"新增系統\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Add URL\"\nmsgstr \"添加 URL\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust display options for charts.\"\nmsgstr \"調整圖表的顯示選項。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Adjust the width of the main layout\"\nmsgstr \"調整主版面寬度\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Admin\"\nmsgstr \"管理員\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"After\"\nmsgstr \"之後\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"After setting the environment variables, restart your Beszel hub for changes to take effect.\"\nmsgstr \"設置環境變數後，請重新啟動 Beszel hub 以使更改生效。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Agent\"\nmsgstr \"客户端\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Alert History\"\nmsgstr \"警報歷史\"\n\n#: src/components/alerts/alert-button.tsx\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Alerts\"\nmsgstr \"警報\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/containers.tsx\nmsgid \"All Containers\"\nmsgstr \"所有容器\"\n\n#: src/components/alerts/alerts-sheet.tsx\n#: src/components/command-palette.tsx\n#: src/components/routes/home.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"All Systems\"\nmsgstr \"所有系統\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Are you sure you want to delete {name}?\"\nmsgstr \"您確定要刪除 {name} 嗎？\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Are you sure?\"\nmsgstr \"您確定嗎？\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Automatic copy requires a secure context.\"\nmsgstr \"自動複製需要安全的上下文。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average\"\nmsgstr \"平均\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average CPU utilization of containers\"\nmsgstr \"容器的平均 CPU 使用率\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average drops below <0>{value}{0}</0>\"\nmsgstr \"平均值降至<0>{value}{0}</0>以下\"\n\n#. placeholder {0}: alertData.unit\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Average exceeds <0>{value}{0}</0>\"\nmsgstr \"平均值超過 <0>{value}{0}</0>\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average power consumption of GPUs\"\nmsgstr \"GPU 的平均功耗\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average system-wide CPU utilization\"\nmsgstr \"系統的平均 CPU 使用率\"\n\n#. placeholder {0}: gpu.n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of {0}\"\nmsgstr \"{0} 的平均使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Average utilization of GPU engines\"\nmsgstr \"GPU 引擎的平均利用率\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Backups\"\nmsgstr \"備份\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Bandwidth\"\nmsgstr \"帶寬\"\n\n#. Battery label in systems table header\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Bat\"\nmsgstr \"電池\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Battery\"\nmsgstr \"電池\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became active\"\nmsgstr \"變為活動\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Became inactive\"\nmsgstr \"變為非活動\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Before\"\nmsgstr \"之前\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Below {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在過去的{2, plural, one {# 分鐘} other {# 分鐘}}中低於{0}{1}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Beszel supports OpenID Connect and many OAuth2 authentication providers.\"\nmsgstr \"Beszel支持OpenID Connect和許多OAuth2認證提供者。\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Beszel uses <0>Shoutrrr</0> to integrate with popular notification services.\"\nmsgstr \"Beszel 使用 <0>Shoutrrr</0> 與流行的通知服務集成。\"\n\n#: src/components/add-system.tsx\nmsgid \"Binary\"\nmsgstr \"執行檔\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bits (Kbps, Mbps, Gbps)\"\nmsgstr \"位元 (Kbps, Mbps, Gbps)\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Boot state\"\nmsgstr \"啟動狀態\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Bytes (KB/s, MB/s, GB/s)\"\nmsgstr \"位元組 (KB/s, MB/s, GB/s)\"\n\n#: src/components/charts/mem-chart.tsx\nmsgid \"Cache / Buffers\"\nmsgstr \"快取 / 緩衝區\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can reload\"\nmsgstr \"可重載\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can start\"\nmsgstr \"可啟動\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Can stop\"\nmsgstr \"可停止\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Capabilities\"\nmsgstr \"能力\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Capacity\"\nmsgstr \"容量\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Caution - potential data loss\"\nmsgstr \"注意 - 可能遺失資料\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Celsius (°C)\"\nmsgstr \"攝氏 (°C)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change display units for metrics.\"\nmsgstr \"更改指標的顯示單位。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Change general application options.\"\nmsgstr \"更改一般應用選項。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Charge\"\nmsgstr \"充電\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Charging\"\nmsgstr \"充電中\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Chart options\"\nmsgstr \"圖表選項\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Check {email} for a reset link.\"\nmsgstr \"檢查 {email} 以獲取重置鏈接。\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Check logs for more details.\"\nmsgstr \"檢查日誌以取得更多資訊。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Check your monitoring service\"\nmsgstr \"檢查您的監控服務\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Check your notification service\"\nmsgstr \"檢查您的通知服務\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Click on a container to view more information.\"\nmsgstr \"點擊容器以查看更多資訊。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Click on a device to view more information.\"\nmsgstr \"點擊裝置以查看更多資訊。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Click on a system to view more information.\"\nmsgstr \"點擊系統以查看更多資訊。\"\n\n#: src/components/ui/input-copy.tsx\nmsgid \"Click to copy\"\nmsgstr \"點擊以複製\"\n\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Command line instructions\"\nmsgstr \"命令行指令\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Configure how you receive alert notifications.\"\nmsgstr \"配置您接收警報通知的方式。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Confirm password\"\nmsgstr \"確認密碼\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Conflicts\"\nmsgstr \"衝突\"\n\n#: src/components/active-alerts.tsx\nmsgid \"Connection is down\"\nmsgstr \"連線中斷\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Continue\"\nmsgstr \"繼續\"\n\n#: src/lib/utils.ts\nmsgid \"Copied to clipboard\"\nmsgstr \"已複製到剪貼板\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker compose file content\"\nmsgid \"Copy docker compose\"\nmsgstr \"複製 docker compose\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy docker run command\"\nmsgid \"Copy docker run\"\nmsgstr \"複製 docker run\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Environment variables\"\nmsgid \"Copy env\"\nmsgstr \"複製環境變數\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy host\"\nmsgstr \"複製主機\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy Linux command\"\nmsgstr \"複製 Linux 指令\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Copy name\"\nmsgstr \"複製名稱\"\n\n#: src/components/copy-to-clipboard.tsx\nmsgid \"Copy text\"\nmsgstr \"複製文本\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the installation command for the agent below, or register agents automatically with a <0>universal token</0>.\"\nmsgstr \"複製下面的代理程式安裝指令，或使用<0>通用令牌</0>自動註冊代理程式。\"\n\n#: src/components/add-system.tsx\nmsgid \"Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>.\"\nmsgstr \"複製下面的代理程式<0>docker-compose.yml</0>內容，或使用<1>通用令牌</1>自動註冊代理程式。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Copy YAML\"\nmsgstr \"複製YAML\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"CPU\"\nmsgstr \"處理器\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Cores\"\nmsgstr \"CPU 核心\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"CPU Peak\"\nmsgstr \"CPU 峰值\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"CPU time\"\nmsgstr \"CPU 時間\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"CPU Time Breakdown\"\nmsgstr \"CPU 時間細分\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/lib/alerts.ts\nmsgid \"CPU Usage\"\nmsgstr \"CPU 使用率\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Create\"\nmsgstr \"建立\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Create account\"\nmsgstr \"創建帳戶\"\n\n#. Context: date created\n#: src/components/alerts-history-columns.tsx\nmsgid \"Created\"\nmsgstr \"已建立\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Critical (%)\"\nmsgstr \"嚴重 (%)\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Download\"\nmsgstr \"累計下載\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Cumulative Upload\"\nmsgstr \"累計上傳\"\n\n#. Context: Battery state\n#: src/components/routes/system.tsx\nmsgid \"Current state\"\nmsgstr \"目前狀態\"\n\n#. Power Cycles\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Cycles\"\nmsgstr \"週期\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Daily\"\nmsgstr \"每日\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Default time period\"\nmsgstr \"預設時間段\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Delete\"\nmsgstr \"刪除\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Delete fingerprint\"\nmsgstr \"刪除指紋\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Description\"\nmsgstr \"描述\"\n\n#: src/components/containers-table/containers-table.tsx\nmsgid \"Detail\"\nmsgstr \"詳細資訊\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Device\"\nmsgstr \"裝置\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Discharging\"\nmsgstr \"放電中\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Disk\"\nmsgstr \"磁碟\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk I/O\"\nmsgstr \"磁碟 I/O\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Disk unit\"\nmsgstr \"磁碟單位\"\n\n#: src/components/charts/disk-chart.tsx\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Disk Usage\"\nmsgstr \"磁碟使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Disk usage of {extraFsName}\"\nmsgstr \"{extraFsName} 的磁碟使用量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker CPU Usage\"\nmsgstr \"Docker CPU 使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Memory Usage\"\nmsgstr \"Docker 記憶體使用率\"\n\n#: src/components/routes/system.tsx\nmsgid \"Docker Network I/O\"\nmsgstr \"Docker 網絡 I/O\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Documentation\"\nmsgstr \"文件\"\n\n#. Context: System is down\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"Down\"\nmsgstr \"中斷\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Down ({downSystemsLength})\"\nmsgstr \"中斷 ({downSystemsLength})\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Download\"\nmsgstr \"下載\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Duration\"\nmsgstr \"持續時間\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Edit\"\nmsgstr \"編輯\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Edit {foo}\"\nmsgstr \"編輯 {foo}\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\n#: src/components/login/otp-forms.tsx\nmsgid \"Email\"\nmsgstr \"電子郵件\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Email notifications\"\nmsgstr \"電子郵件通知\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Empty\"\nmsgstr \"空電\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"End Time\"\nmsgstr \"結束時間\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL\"\nmsgstr \"端點 URL\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Endpoint URL to ping (required)\"\nmsgstr \"要 ping 的端點 URL (必填)\"\n\n#: src/components/login/login.tsx\nmsgid \"Enter email address to reset password\"\nmsgstr \"輸入電子郵件地址以重置密碼\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Enter email address...\"\nmsgstr \"輸入電子郵件地址...\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Enter your one-time password.\"\nmsgstr \"輸入您的一次性密碼。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Ephemeral\"\nmsgstr \"臨時\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/settings/config-yaml.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/heartbeat.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Error\"\nmsgstr \"錯誤\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Example:\"\nmsgstr \"範例：\"\n\n#. placeholder {0}: alert.value\n#. placeholder {1}: info.unit\n#. placeholder {2}: alert.min\n#: src/components/active-alerts.tsx\nmsgid \"Exceeds {0}{1} in last {2, plural, one {# minute} other {# minutes}}\"\nmsgstr \"在過去的{2, plural, one {# 分鐘} other {# 分鐘}}中超過{0}{1}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exec main PID\"\nmsgstr \"執行主進程 ID\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Existing systems not defined in <0>config.yml</0> will be deleted. Please make regular backups.\"\nmsgstr \"未在<0>config.yml</0>中定義的現有系統將被刪除。請定期備份。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Exited active\"\nmsgstr \"退出活動狀態\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Expires after one hour or on hub restart.\"\nmsgstr \"一小時後或重新啟動集線器時過期。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Export\"\nmsgstr \"匯出\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export configuration\"\nmsgstr \"匯出設定\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Export your current systems configuration.\"\nmsgstr \"匯出您現在的系統設定。\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Fahrenheit (°F)\"\nmsgstr \"華氏 (°F)\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Failed\"\nmsgstr \"失敗\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Failed Attributes:\"\nmsgstr \"失敗屬性：\"\n\n#: src/lib/api.ts\nmsgid \"Failed to authenticate\"\nmsgstr \"認證失敗\"\n\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Failed to save settings\"\nmsgstr \"儲存設定失敗\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Failed to send heartbeat\"\nmsgstr \"發送 heartbeat 失敗\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Failed to send test notification\"\nmsgstr \"發送測試通知失敗\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Failed to update alert\"\nmsgstr \"更新警報失敗\"\n\n#. placeholder {0}: statusTotals[ServiceStatus.Failed]\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Failed: {0}\"\nmsgstr \"失敗: {0}\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Filter...\"\nmsgstr \"篩選...\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Fingerprint\"\nmsgstr \"指紋\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Firmware\"\nmsgstr \"韌體\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"For <0>{min}</0> {min, plural, one {minute} other {minutes}}\"\nmsgstr \"持續<0>{min}</0> {min, plural, one {分鐘} other {分鐘}}\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Forgot password?\"\nmsgstr \"忘記密碼？\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"FreeBSD command\"\nmsgstr \"FreeBSD 指令\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Full\"\nmsgstr \"滿電\"\n\n#. Context: General settings\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"General\"\nmsgstr \"一般\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Global\"\nmsgstr \"全域\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Engines\"\nmsgstr \"GPU 引擎\"\n\n#: src/components/routes/system.tsx\nmsgid \"GPU Power Draw\"\nmsgstr \"GPU 功耗\"\n\n#: src/lib/alerts.ts\nmsgid \"GPU Usage\"\nmsgstr \"GPU 使用率\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Grid\"\nmsgstr \"網格\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgid \"Health\"\nmsgstr \"健康狀態\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Heartbeat\"\nmsgstr \"Heartbeat\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat Monitoring\"\nmsgstr \"Heartbeat 監控\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Heartbeat sent successfully\"\nmsgstr \"Heartbeat 發送成功\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Homebrew command\"\nmsgstr \"Homebrew 指令\"\n\n#: src/components/add-system.tsx\nmsgid \"Host / IP\"\nmsgstr \"主機 / IP\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP Method\"\nmsgstr \"HTTP 方法\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"HTTP method: POST, GET, or HEAD (default: POST)\"\nmsgstr \"HTTP 方法：POST、GET 或 HEAD (預設：POST)\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Idle\"\nmsgstr \"閒置\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"If you've lost the password to your admin account, you may reset it using the following command.\"\nmsgstr \"如果您遺失了管理員帳號密碼，可以使用以下指令重設。\"\n\n#: src/components/containers-table/containers-table-columns.tsx\nmsgctxt \"Docker image\"\nmsgid \"Image\"\nmsgstr \"鏡像\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Inactive\"\nmsgstr \"未啟用\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Interval\"\nmsgstr \"間隔\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Invalid email address.\"\nmsgstr \"無效的電子郵件地址。\"\n\n#: src/components/lang-toggle.tsx\n#: src/components/routes/settings/general.tsx\nmsgid \"Language\"\nmsgstr \"語言\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Layout\"\nmsgstr \"版面配置\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Layout width\"\nmsgstr \"版面寬度\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Lifecycle\"\nmsgstr \"生命週期\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"limit\"\nmsgstr \"限制\"\n\n#: src/components/routes/system.tsx\nmsgid \"Load Average\"\nmsgstr \"平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 15m\"\nmsgstr \"15分鐘平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 1m\"\nmsgstr \"1分鐘平均負載\"\n\n#: src/lib/alerts.ts\nmsgid \"Load Average 5m\"\nmsgstr \"5分鐘平均負載\"\n\n#. Short label for load average\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Load Avg\"\nmsgstr \"平均負載\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Load state\"\nmsgstr \"載入狀態\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Loading...\"\nmsgstr \"載入中...\"\n\n#: src/components/navbar.tsx\nmsgid \"Log Out\"\nmsgstr \"登出\"\n\n#: src/components/login/login.tsx\nmsgid \"Login\"\nmsgstr \"登入\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Login attempt failed\"\nmsgstr \"登入嘗試失敗\"\n\n#: src/components/command-palette.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/navbar.tsx\nmsgid \"Logs\"\nmsgstr \"日誌\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Looking instead for where to create alerts? Click the bell <0/> icons in the systems table.\"\nmsgstr \"在尋找創建警報的位置嗎？點擊系統表中的鈴鐺<0/>。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Main PID\"\nmsgstr \"主進程 ID\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Manage display and notification preferences.\"\nmsgstr \"管理顯示和通知偏好。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Manual setup instructions\"\nmsgstr \"手動設定說明\"\n\n#. Chart select field. Please try to keep this short.\n#: src/components/routes/system.tsx\nmsgid \"Max 1 min\"\nmsgstr \"一分鐘內最大值\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Memory\"\nmsgstr \"記憶體\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory limit\"\nmsgstr \"記憶體限制\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Memory Peak\"\nmsgstr \"記憶體峰值\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Memory Usage\"\nmsgstr \"記憶體使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Memory usage of docker containers\"\nmsgstr \"Docker 容器的記憶體使用量\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Model\"\nmsgstr \"型號\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Name\"\nmsgstr \"名稱\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Net\"\nmsgstr \"網絡\"\n\n#: src/components/routes/system.tsx\nmsgid \"Network traffic of docker containers\"\nmsgstr \"Docker 容器的網絡流量\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Network traffic of public interfaces\"\nmsgstr \"公共接口的網絡流量\"\n\n#. Context: Bytes or bits\n#: src/components/routes/settings/general.tsx\nmsgid \"Network unit\"\nmsgstr \"網路單位\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No\"\nmsgstr \"否\"\n\n#: src/components/command-palette.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results found.\"\nmsgstr \"未找到結果。\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"No results.\"\nmsgstr \"沒有結果。\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"No S.M.A.R.T. attributes available for this device.\"\nmsgstr \"此裝置沒有可用的 S.M.A.R.T. 屬性。\"\n\n#: src/components/systems-table/systems-table.tsx\n#: src/components/systems-table/systems-table.tsx\nmsgid \"No systems found.\"\nmsgstr \"未找到系統。\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Notifications\"\nmsgstr \"通知\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"OAuth 2 / OIDC support\"\nmsgstr \"支援 OAuth 2 / OIDC\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"On each restart, systems in the database will be updated to match the systems defined in the file.\"\nmsgstr \"每次重新啟動時，將會以檔案中的系統定義更新資料庫。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"One-time\"\nmsgstr \"一次性\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"One-time password\"\nmsgstr \"一次性密碼\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Open menu\"\nmsgstr \"開啟選單\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Or continue with\"\nmsgstr \"或繼續使用\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Other\"\nmsgstr \"其他\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Overwrite existing alerts\"\nmsgstr \"覆蓋現有警報\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\nmsgid \"Page\"\nmsgstr \"頁面\"\n\n#. placeholder {0}: table.getState().pagination.pageIndex + 1\n#. placeholder {1}: table.getPageCount()\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Page {0} of {1}\"\nmsgstr \"第 {0} 頁，共 {1} 頁\"\n\n#: src/components/command-palette.tsx\nmsgid \"Pages / Settings\"\nmsgstr \"頁面 / 設定\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/auth-form.tsx\nmsgid \"Password\"\nmsgstr \"密碼\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be at least 8 characters.\"\nmsgstr \"密碼必須至少包含 8 個字符。\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Password must be less than 72 bytes.\"\nmsgstr \"密碼必須少於 72 個字節。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Password reset request received\"\nmsgstr \"已收到密碼重設請求\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Past\"\nmsgstr \"過去\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Pause\"\nmsgstr \"暫停\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Paused\"\nmsgstr \"已暫停\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Paused ({pausedSystemsLength})\"\nmsgstr \"已暫停 ({pausedSystemsLength})\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Payload format\"\nmsgstr \"負載 (Payload) 格式\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Per-core average utilization\"\nmsgstr \"每個核心的平均使用率\"\n\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Percentage of time spent in each state\"\nmsgstr \"在每個狀態下花費的時間百分比\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Permanent\"\nmsgstr \"永久\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Persistence\"\nmsgstr \"持久性\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Please <0>configure an SMTP server</0> to ensure alerts are delivered.\"\nmsgstr \"請<0>配置SMTP伺服器</0>以確保警報被傳送。\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"Please check logs for more details.\"\nmsgstr \"請檢查日誌以獲取更多資訊。\"\n\n#: src/components/login/auth-form.tsx\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Please check your credentials and try again\"\nmsgstr \"請檢查您的憑證並重試\"\n\n#: src/components/login/login.tsx\nmsgid \"Please create an admin account\"\nmsgstr \"請建立一個管理員帳號\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please enable pop-ups for this site\"\nmsgstr \"請為此網站啟用彈出窗口\"\n\n#: src/lib/api.ts\nmsgid \"Please log in again\"\nmsgstr \"請重新登入\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Please see <0>the documentation</0> for instructions.\"\nmsgstr \"請參閱<0>文件</0>以取得說明。\"\n\n#: src/components/login/login.tsx\nmsgid \"Please sign in to your account\"\nmsgstr \"請登入您的帳號\"\n\n#: src/components/add-system.tsx\nmsgid \"Port\"\nmsgstr \"端口\"\n\n#. Power On Time\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Power On\"\nmsgstr \"電源開啟\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Precise utilization at the recorded time\"\nmsgstr \"記錄時間的精確使用率\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Preferred Language\"\nmsgstr \"首選語言\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Process started\"\nmsgstr \"進程啟動\"\n\n#. Use 'Key' if your language requires many more characters\n#: src/components/add-system.tsx\nmsgid \"Public Key\"\nmsgstr \"公鑰\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Quiet Hours\"\nmsgstr \"靜音時段\"\n\n#. Disk read\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Read\"\nmsgstr \"讀取\"\n\n#: src/components/routes/system.tsx\nmsgid \"Received\"\nmsgstr \"接收\"\n\n#: src/components/containers-table/containers-table.tsx\n#: src/components/containers-table/containers-table.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Refresh\"\nmsgstr \"重新整理\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Relationships\"\nmsgstr \"關係\"\n\n#: src/components/login/login.tsx\nmsgid \"Request a one-time password\"\nmsgstr \"請求一次性密碼\"\n\n#: src/components/login/otp-forms.tsx\nmsgid \"Request OTP\"\nmsgstr \"請求 OTP\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Required by\"\nmsgstr \"被需要\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Requires\"\nmsgstr \"需要\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Reset Password\"\nmsgstr \"重設密碼\"\n\n#: src/components/alerts-history-columns.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Resolved\"\nmsgstr \"已解決\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Restarts\"\nmsgstr \"重啟次數\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Resume\"\nmsgstr \"恢復\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgctxt \"Root disk label\"\nmsgid \"Root\"\nmsgstr \"根\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Rotate token\"\nmsgstr \"輪換令牌\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"Rows per page\"\nmsgstr \"每頁行數\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Runtime Metrics\"\nmsgstr \"運行時指標\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Details\"\nmsgstr \"S.M.A.R.T. 詳情\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"S.M.A.R.T. Self-Test\"\nmsgstr \"S.M.A.R.T. 自檢\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save address using enter key or comma. Leave blank to disable email notifications.\"\nmsgstr \"使用回車鍵或逗號保存地址。留空以禁用電子郵件通知。\"\n\n#: src/components/routes/settings/general.tsx\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Save Settings\"\nmsgstr \"儲存設定\"\n\n#: src/components/add-system.tsx\nmsgid \"Save system\"\nmsgstr \"儲存系統\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Saved in the database and does not expire until you disable it.\"\nmsgstr \"儲存在資料庫中，在您停用之前不會過期。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule\"\nmsgstr \"排程\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent, such as during maintenance periods.\"\nmsgstr \"安排不會發送通知的靜音時段，例如在維護期間。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Schedule quiet hours where notifications will not be sent.\"\nmsgstr \"安排不會發送通知的靜音時段。\"\n\n#: src/components/navbar.tsx\nmsgid \"Search\"\nmsgstr \"搜索\"\n\n#: src/components/command-palette.tsx\nmsgid \"Search for systems or settings...\"\nmsgstr \"搜索系統或設置...\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Seconds between pings (default: 60)\"\nmsgstr \"Ping 之間的秒數 (預設：60)\"\n\n#: src/components/alerts/alerts-sheet.tsx\nmsgid \"See <0>notification settings</0> to configure how you receive alerts.\"\nmsgstr \"查看<0>通知設置</0>以配置您接收警報的方式。\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Select {foo}\"\nmsgstr \"選擇 {foo}\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send a single heartbeat ping to verify your endpoint is working.\"\nmsgstr \"發送單次 heartbeat ping 以驗證您的端點是否正常運作。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send periodic outbound pings to an external monitoring service so you can monitor Beszel without exposing it to the internet.\"\nmsgstr \"定期向外部監控服務發送出站 ping，以便您在不將 Beszel 暴露於網際網路的情況下進行監控。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Send test heartbeat\"\nmsgstr \"發送測試 heartbeat\"\n\n#: src/components/routes/system.tsx\nmsgid \"Sent\"\nmsgstr \"發送\"\n\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Serial Number\"\nmsgstr \"序列號\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Service Details\"\nmsgstr \"服務詳情\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Services\"\nmsgstr \"服務\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Set percentage thresholds for meter colors.\"\nmsgstr \"設定儀表顏色的百分比閾值。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Set the following environment variables on your Beszel hub to enable heartbeat monitoring:\"\nmsgstr \"在您的 Beszel hub 上設置以下環境變數以啟用 heartbeat 監控：\"\n\n#: src/components/command-palette.tsx\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings\"\nmsgstr \"設置\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Settings saved\"\nmsgstr \"設置已保存\"\n\n#: src/components/login/auth-form.tsx\nmsgid \"Sign in\"\nmsgstr \"登錄\"\n\n#: src/components/command-palette.tsx\nmsgid \"SMTP settings\"\nmsgstr \"SMTP設置\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Sort By\"\nmsgstr \"排序依據\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Start Time\"\nmsgstr \"開始時間\"\n\n#. Context: alert state (active or resolved)\n#: src/components/alerts-history-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"State\"\nmsgstr \"狀態\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systems-table/systems-table.tsx\n#: src/lib/alerts.ts\nmsgid \"Status\"\nmsgstr \"狀態\"\n\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Sub State\"\nmsgstr \"子狀態\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap space used by the system\"\nmsgstr \"系統使用的交換空間\"\n\n#: src/components/routes/system.tsx\nmsgid \"Swap Usage\"\nmsgstr \"交換使用\"\n\n#: src/components/add-system.tsx\n#: src/components/alerts-history-columns.tsx\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\n#: src/lib/alerts.ts\nmsgid \"System\"\nmsgstr \"系統\"\n\n#: src/components/routes/system.tsx\nmsgid \"System load averages over time\"\nmsgstr \"系統平均負載隨時間變化\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Systemd Services\"\nmsgstr \"Systemd 服務\"\n\n#: src/components/navbar.tsx\nmsgid \"Systems\"\nmsgstr \"系統\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"Systems may be managed in a <0>config.yml</0> file inside your data directory.\"\nmsgstr \"系統可以在您的數據目錄中的<0>config.yml</0>文件中管理。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Table\"\nmsgstr \"表格\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Tasks\"\nmsgstr \"任務數\"\n\n#. Temperature label in systems table\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Temp\"\nmsgstr \"溫度\"\n\n#: src/components/routes/system.tsx\n#: src/lib/alerts.ts\nmsgid \"Temperature\"\nmsgstr \"溫度\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Temperature unit\"\nmsgstr \"溫度單位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Temperatures of system sensors\"\nmsgstr \"系統傳感器的溫度\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test <0>URL</0>\"\nmsgstr \"測試<0>URL</0>\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"Test heartbeat\"\nmsgstr \"測試 heartbeat\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Test notification sent\"\nmsgstr \"測試通知已發送\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"The overall status is <0>ok</0> when all systems are up, <1>warn</1> when alerts are triggered, and <2>error</2> when any system is down.\"\nmsgstr \"當所有系統都正常運行時，整體狀態為 <0>正常</0>；當觸發警報時為 <1>警告</1>；當任何系統故障時為 <2>錯誤</2>。\"\n\n#: src/components/login/forgot-pass-form.tsx\nmsgid \"Then log into the backend and reset your user account password in the users table.\"\nmsgstr \"然後登錄到後端並在用戶表中重置您的用戶帳戶密碼。\"\n\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"This action cannot be undone. This will permanently delete all current records for {name} from the database.\"\nmsgstr \"此操作無法撤銷。這將永久刪除數據庫中{name}的所有當前記錄。\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"This will permanently delete all selected records from the database.\"\nmsgstr \"這將從資料庫中永久刪除所有選定的記錄。\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of {extraFsName}\"\nmsgstr \"{extraFsName}的吞吐量\"\n\n#: src/components/routes/system.tsx\nmsgid \"Throughput of root filesystem\"\nmsgstr \"根文件系統的吞吐量\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Time format\"\nmsgstr \"時間格式\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"To email(s)\"\nmsgstr \"發送到電子郵件\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/routes/system/info-bar.tsx\nmsgid \"Toggle grid\"\nmsgstr \"切換網格\"\n\n#: src/components/mode-toggle.tsx\n#: src/components/mode-toggle.tsx\nmsgid \"Toggle theme\"\nmsgstr \"切換主題\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Token\"\nmsgstr \"令牌\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/layout.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens & Fingerprints\"\nmsgstr \"令牌和指紋\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens allow agents to connect and register. Fingerprints are stable identifiers unique to each system, set on first connection.\"\nmsgstr \"令牌允許代理程式連接和註冊。指紋是每個系統唯一的穩定識別符，在首次連接時設置。\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Tokens and fingerprints are used to authenticate WebSocket connections to the hub.\"\nmsgstr \"令牌和指紋用於驗證到中心的WebSocket連接。\"\n\n#: src/components/ui/chart.tsx\n#: src/components/ui/chart.tsx\nmsgid \"Total\"\nmsgstr \"總計\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data received for each interface\"\nmsgstr \"每個介面的總接收資料量\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Total data sent for each interface\"\nmsgstr \"每個介面的總傳送資料量\"\n\n#. placeholder {0}: data.length\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Total: {0}\"\nmsgstr \"總計: {0}\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggered by\"\nmsgstr \"由...觸發\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Triggers\"\nmsgstr \"觸發器\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 1 minute load average exceeds a threshold\"\nmsgstr \"當 1 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 15 minute load average exceeds a threshold\"\nmsgstr \"當 15 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when 5 minute load average exceeds a threshold\"\nmsgstr \"當 5 分鐘平均負載超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when any sensor exceeds a threshold\"\nmsgstr \"當任何傳感器超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when battery charge drops below a threshold\"\nmsgstr \"當電池電量降至閾值以下時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when combined up/down exceeds a threshold\"\nmsgstr \"當組合的上/下超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when CPU usage exceeds a threshold\"\nmsgstr \"當CPU使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when GPU usage exceeds a threshold\"\nmsgstr \"當 GPU 使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when memory usage exceeds a threshold\"\nmsgstr \"當記憶體使用率超過閾值時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when status switches between up and down\"\nmsgstr \"當狀態在上和下之間切換時觸發\"\n\n#: src/lib/alerts.ts\nmsgid \"Triggers when usage of any disk exceeds a threshold\"\nmsgstr \"當任何磁碟的使用超過閾值時觸發\"\n\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/settings/quiet-hours.tsx\n#: src/components/routes/system/smart-table.tsx\nmsgid \"Type\"\nmsgstr \"類型\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unit file\"\nmsgstr \"單元檔案\"\n\n#. Temperature / network units\n#: src/components/routes/settings/general.tsx\nmsgid \"Unit preferences\"\nmsgstr \"單位偏好\"\n\n#: src/components/command-palette.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"Universal token\"\nmsgstr \"通用令牌\"\n\n#. Context: Battery state\n#: src/lib/i18n.ts\nmsgid \"Unknown\"\nmsgstr \"未知\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Unlimited\"\nmsgstr \"無限制\"\n\n#. Context: System is up\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Up\"\nmsgstr \"上線\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Up ({upSystemsLength})\"\nmsgstr \"上線 ({upSystemsLength})\"\n\n#: src/components/routes/settings/quiet-hours.tsx\nmsgid \"Update\"\nmsgstr \"更新\"\n\n#: src/components/containers-table/containers-table-columns.tsx\n#: src/components/routes/system/smart-table.tsx\n#: src/components/systemd-table/systemd-table-columns.tsx\nmsgid \"Updated\"\nmsgstr \"已更新\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Updated every 10 minutes.\"\nmsgstr \"每 10 分鐘更新一次。\"\n\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"Upload\"\nmsgstr \"上傳\"\n\n#: src/components/routes/system/info-bar.tsx\n#: src/components/systems-table/systems-table-columns.tsx\nmsgid \"Uptime\"\nmsgstr \"運行時間\"\n\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\n#: src/components/routes/system/cpu-sheet.tsx\nmsgid \"Usage\"\nmsgstr \"使用\"\n\n#: src/components/routes/system.tsx\nmsgid \"Usage of root partition\"\nmsgstr \"根分區的使用\"\n\n#: src/components/charts/mem-chart.tsx\n#: src/components/charts/swap-chart.tsx\nmsgid \"Used\"\nmsgstr \"已用\"\n\n#: src/components/command-palette.tsx\n#: src/components/navbar.tsx\nmsgid \"Users\"\nmsgstr \"用戶\"\n\n#: src/components/alerts-history-columns.tsx\nmsgid \"Value\"\nmsgstr \"值\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"View\"\nmsgstr \"檢視\"\n\n#: src/components/routes/system/cpu-sheet.tsx\n#: src/components/routes/system/network-sheet.tsx\nmsgid \"View more\"\nmsgstr \"查看更多\"\n\n#: src/components/routes/settings/alerts-history-data-table.tsx\nmsgid \"View your 200 most recent alerts.\"\nmsgstr \"檢視最近 200 則警報。\"\n\n#: src/components/systems-table/systems-table.tsx\nmsgid \"Visible Fields\"\nmsgstr \"可見欄位\"\n\n#: src/components/routes/system.tsx\nmsgid \"Waiting for enough records to display\"\nmsgstr \"等待足夠的記錄以顯示\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Want to help improve our translations? Check <0>Crowdin</0> for details.\"\nmsgstr \"想幫助我們改進翻譯嗎？查看<0>Crowdin</0>以獲取更多詳細信息。\"\n\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Wants\"\nmsgstr \"希望\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning (%)\"\nmsgstr \"警告 (%)\"\n\n#: src/components/routes/settings/general.tsx\nmsgid \"Warning thresholds\"\nmsgstr \"警告閾值\"\n\n#: src/components/routes/settings/notifications.tsx\nmsgid \"Webhook / Push notifications\"\nmsgstr \"Webhook / 推送通知\"\n\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgid \"When enabled, this token allows agents to self-register without prior system creation.\"\nmsgstr \"啟用後，此權杖允許代理無需事先建立系統即可自行註冊。\"\n\n#: src/components/routes/settings/heartbeat.tsx\nmsgid \"When using POST, each heartbeat includes a JSON payload with system status summary, list of down systems, and triggered alerts.\"\nmsgstr \"使用 POST 時，每個 heartbeat 都包含一個 JSON 負載，其中包含系統狀態摘要、故障系統列表和觸發的警報。\"\n\n#: src/components/add-system.tsx\n#: src/components/routes/settings/tokens-fingerprints.tsx\nmsgctxt \"Button to copy install command\"\nmsgid \"Windows command\"\nmsgstr \"Windows 指令\"\n\n#. Disk write\n#: src/components/routes/system.tsx\n#: src/components/routes/system.tsx\nmsgid \"Write\"\nmsgstr \"寫入\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"YAML Config\"\nmsgstr \"YAML配置\"\n\n#: src/components/routes/settings/config-yaml.tsx\nmsgid \"YAML Configuration\"\nmsgstr \"YAML配置\"\n\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\n#: src/components/systemd-table/systemd-table.tsx\nmsgid \"Yes\"\nmsgstr \"是\"\n\n#: src/components/routes/settings/layout.tsx\nmsgid \"Your user settings have been updated.\"\nmsgstr \"您的用戶設置已更新。\"\n"
  },
  {
    "path": "internal/site/src/main.tsx",
    "content": "import \"./index.css\"\nimport { i18n } from \"@lingui/core\"\nimport { I18nProvider } from \"@lingui/react\"\nimport { useStore } from \"@nanostores/react\"\nimport { DirectionProvider } from \"@radix-ui/react-direction\"\n// import { Suspense, lazy, useEffect, StrictMode } from \"react\"\nimport { lazy, memo, Suspense, useEffect } from \"react\"\nimport ReactDOM from \"react-dom/client\"\nimport Navbar from \"@/components/navbar.tsx\"\nimport { $router } from \"@/components/router.tsx\"\nimport Settings from \"@/components/routes/settings/layout.tsx\"\nimport { ThemeProvider } from \"@/components/theme-provider.tsx\"\nimport { Toaster } from \"@/components/ui/toaster.tsx\"\nimport { alertManager } from \"@/lib/alerts\"\nimport { pb, updateUserSettings } from \"@/lib/api.ts\"\nimport { dynamicActivate, getLocale } from \"@/lib/i18n\"\nimport {\n\t$authenticated,\n\t$copyContent,\n\t$direction,\n\t$publicKey,\n\t$userSettings,\n\tdefaultLayoutWidth,\n} from \"@/lib/stores.ts\"\nimport * as systemsManager from \"@/lib/systemsManager.ts\"\n\nconst LoginPage = lazy(() => import(\"@/components/login/login.tsx\"))\nconst Home = lazy(() => import(\"@/components/routes/home.tsx\"))\nconst Containers = lazy(() => import(\"@/components/routes/containers.tsx\"))\nconst Smart = lazy(() => import(\"@/components/routes/smart.tsx\"))\nconst SystemDetail = lazy(() => import(\"@/components/routes/system.tsx\"))\nconst CopyToClipboardDialog = lazy(() => import(\"@/components/copy-to-clipboard.tsx\"))\n\nconst App = memo(() => {\n\tconst page = useStore($router)\n\n\tuseEffect(() => {\n\t\t// change auth store on auth change\n\t\tpb.authStore.onChange(() => {\n\t\t\t$authenticated.set(pb.authStore.isValid)\n\t\t})\n\t\t// get version / public key\n\t\tpb.send(\"/api/beszel/getkey\", {}).then((data) => {\n\t\t\t$publicKey.set(data.key)\n\t\t})\n\t\t// get user settings\n\t\tupdateUserSettings()\n\t\t// need to get system list before alerts\n\t\tsystemsManager.init()\n\t\tsystemsManager\n\t\t\t// get current systems list\n\t\t\t.refresh()\n\t\t\t// subscribe to new system updates\n\t\t\t.then(systemsManager.subscribe)\n\t\t\t// get current alerts\n\t\t\t.then(alertManager.refresh)\n\t\t\t// subscribe to new alert updates\n\t\t\t.then(alertManager.subscribe)\n\t\treturn () => {\n\t\t\talertManager.unsubscribe()\n\t\t\tsystemsManager.unsubscribe()\n\t\t}\n\t}, [])\n\n\tif (!page) {\n\t\treturn <h1 className=\"text-3xl text-center my-14\">404</h1>\n\t} else if (page.route === \"home\") {\n\t\treturn <Home />\n\t} else if (page.route === \"system\") {\n\t\treturn <SystemDetail id={page.params.id} />\n\t} else if (page.route === \"containers\") {\n\t\treturn <Containers />\n\t} else if (page.route === \"smart\") {\n\t\treturn <Smart />\n\t} else if (page.route === \"settings\") {\n\t\treturn <Settings />\n\t}\n})\n\nconst Layout = () => {\n\tconst authenticated = useStore($authenticated)\n\tconst copyContent = useStore($copyContent)\n\tconst direction = useStore($direction)\n\tconst userSettings = useStore($userSettings)\n\n\tuseEffect(() => {\n\t\tdocument.documentElement.dir = direction\n\t}, [direction])\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: only run on mount\n\tuseEffect(() => {\n\t\t// refresh auth if not authenticated (required for trusted auth header)\n\t\tif (!authenticated) {\n\t\t\tpb.collection(\"users\")\n\t\t\t\t.authRefresh()\n\t\t\t\t.then((res) => {\n\t\t\t\t\tpb.authStore.save(res.token, res.record)\n\t\t\t\t\t$authenticated.set(!!pb.authStore.isValid)\n\t\t\t\t})\n\t\t}\n\t}, [])\n\n\treturn (\n\t\t<DirectionProvider dir={direction}>\n\t\t\t{!authenticated ? (\n\t\t\t\t<Suspense>\n\t\t\t\t\t<LoginPage />\n\t\t\t\t</Suspense>\n\t\t\t) : (\n\t\t\t\t<div style={{ \"--container\": `${userSettings.layoutWidth ?? defaultLayoutWidth}px` } as React.CSSProperties}>\n\t\t\t\t\t<div className=\"container\">\n\t\t\t\t\t\t<Navbar />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"container relative\">\n\t\t\t\t\t\t<App />\n\t\t\t\t\t\t{copyContent && (\n\t\t\t\t\t\t\t<Suspense>\n\t\t\t\t\t\t\t\t<CopyToClipboardDialog content={copyContent} />\n\t\t\t\t\t\t\t</Suspense>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</DirectionProvider>\n\t)\n}\n\nconst I18nApp = () => {\n\tuseEffect(() => {\n\t\tdynamicActivate(getLocale())\n\t}, [])\n\n\treturn (\n\t\t<I18nProvider i18n={i18n}>\n\t\t\t<ThemeProvider>\n\t\t\t\t<Layout />\n\t\t\t\t<Toaster />\n\t\t\t</ThemeProvider>\n\t\t</I18nProvider>\n\t)\n}\n\nReactDOM.createRoot(document.getElementById(\"app\") as HTMLElement).render(\n\t// strict mode in dev mounts / unmounts components twice\n\t// and breaks the clipboard dialog\n\t//<StrictMode>\n\t<I18nApp />\n\t//</StrictMode>\n)\n"
  },
  {
    "path": "internal/site/src/types.d.ts",
    "content": "import type { RecordModel } from \"pocketbase\"\nimport type { Unit, Os, BatteryState, HourFormat, ConnectionType, ServiceStatus, ServiceSubState } from \"@/lib/enums\"\n\n// global window properties\ndeclare global {\n\tvar BESZEL: {\n\t\tBASE_PATH: string\n\t\tHUB_VERSION: string\n\t\tHUB_URL: string\n\t}\n}\n\nexport interface FingerprintRecord extends RecordModel {\n\tid: string\n\tsystem: string\n\tfingerprint: string\n\ttoken: string\n\texpand: {\n\t\tsystem: {\n\t\t\tname: string\n\t\t}\n\t}\n}\n\nexport interface SystemRecord extends RecordModel {\n\tname: string\n\thost: string\n\tstatus: \"up\" | \"down\" | \"paused\" | \"pending\"\n\tport: string\n\tinfo: SystemInfo\n\tv: string\n\tupdated: string\n}\n\nexport interface SystemInfo {\n\t/** hostname */\n\th: string\n\t/** kernel **/\n\tk?: string\n\t/** cpu percent */\n\tcpu: number\n\t/** cpu threads */\n\tt?: number\n\t/** cpu cores */\n\tc: number\n\t/** cpu model */\n\tm: string\n\t/** load average */\n\tla?: [number, number, number]\n\t/** operating system */\n\to?: string\n\t/** uptime */\n\tu: number\n\t/** memory percent */\n\tmp: number\n\t/** disk percent */\n\tdp: number\n\t/** battery percent and state */\n\tbat?: [number, BatteryState]\n\t/** bandwidth (mb) */\n\tb: number\n\t/** bandwidth bytes */\n\tbb?: number\n\t/** agent version */\n\tv: string\n\t/** system is using podman */\n\tp?: boolean\n\t/** highest gpu utilization */\n\tg?: number\n\t/** dashboard display temperature */\n\tdt?: number\n\t/** operating system */\n\tos?: Os\n\t/** connection type */\n\tct?: ConnectionType\n\t/** extra filesystem percentages */\n\tefs?: Record<string, number>\n\t/** services [totalServices, numFailedServices] */\n\tsv?: [number, number]\n}\n\nexport interface SystemStats {\n\t/** cpu percent */\n\tcpu: number\n\t/** peak cpu */\n\tcpum?: number\n\t/** cpu breakdown [user, system, iowait, steal, idle] (0-100 integers) */\n\tcpub?: number[]\n\t/** per-core cpu usage [CPU0..] (0-100 integers) */\n\tcpus?: number[]\n\t/** load average */\n\tla?: [number, number, number]\n\t/** total memory (gb) */\n\tm: number\n\t/** memory used (gb) */\n\tmu: number\n\t/** memory percent */\n\tmp: number\n\t/** memory buffer + cache (gb) */\n\tmb: number\n\t/** max used memory (gb) */\n\tmm?: number\n\t/** zfs arc memory (gb) */\n\tmz?: number\n\t/** swap space (gb) */\n\ts: number\n\t/** swap used (gb) */\n\tsu: number\n\t/** disk size (gb) */\n\td: number\n\t/** disk used (gb) */\n\tdu: number\n\t/** disk percent */\n\tdp: number\n\t/** disk read (mb) */\n\tdr: number\n\t/** disk write (mb) */\n\tdw: number\n\t/** max disk read (mb) */\n\tdrm?: number\n\t/** max disk write (mb) */\n\tdwm?: number\n\t/** disk I/O bytes [read, write] */\n\tdio?: [number, number]\n\t/** max disk I/O bytes [read, write] */\n\tdiom?: [number, number]\n\t/** network sent (mb) */\n\tns: number\n\t/** network received (mb) */\n\tnr: number\n\t/** bandwidth bytes [sent, recv] */\n\tb?: [number, number]\n\t/** max network sent (mb) */\n\tnsm?: number\n\t/** max network received (mb) */\n\tnrm?: number\n\t/** max network sent (bytes) */\n\tbm?: [number, number]\n\t/** temperatures */\n\tt?: Record<string, number>\n\t/** extra filesystems */\n\tefs?: Record<string, ExtraFsStats>\n\t/** GPU data */\n\tg?: Record<string, GPUData>\n\t/** battery percent and state */\n\tbat?: [number, BatteryState]\n\t/** network interfaces [upload bytes, download bytes, total upload bytes, total download bytes] */\n\tni?: Record<string, [number, number, number, number]>\n}\n\nexport interface GPUData {\n\t/** name */\n\tn: string\n\t/** memory used (mb) */\n\tmu?: number\n\t/** memory total (mb) */\n\tmt?: number\n\t/** usage (%) */\n\tu: number\n\t/** power (w) */\n\tp?: number\n\t/** power package (w) */\n\tpp?: number\n\t/** engines */\n\te?: Record<string, number>\n}\n\nexport interface ExtraFsStats {\n\t/** disk size (gb) */\n\td: number\n\t/** disk used (gb) */\n\tdu: number\n\t/** total read (mb) */\n\tr: number\n\t/** total write (mb) */\n\tw: number\n\t/** max read (mb) */\n\trm: number\n\t/** max write (mb) */\n\twm: number\n\t/** read per second (bytes) */\n\trb: number\n\t/** write per second (bytes) */\n\twb: number\n\t/** max read per second (bytes) */\n\trbm: number\n\t/** max write per second (mb) */\n\twbm: number\n}\n\nexport interface ContainerStatsRecord extends RecordModel {\n\tsystem: string\n\tstats: ContainerStats[]\n\tcreated: string | number\n}\n\ninterface ContainerStats {\n\t/** name */\n\tn: string\n\t/** cpu percent */\n\tc: number\n\t/** memory used (gb) */\n\tm: number\n\t// network sent (mb)\n\tns?: number\n\t// network received (mb)\n\tnr?: number\n\t/** bandwidth bytes [sent, recv] */\n\tb?: [number, number]\n}\n\nexport interface SystemStatsRecord extends RecordModel {\n\tsystem: string\n\tstats: SystemStats\n\tcreated: string | number\n}\n\nexport interface AlertRecord extends RecordModel {\n\tid: string\n\tsystem: string\n\tname: string\n\ttriggered: boolean\n\tvalue: number\n\tmin: number\n\t// user: string\n}\n\nexport interface AlertsHistoryRecord extends RecordModel {\n\talert: string\n\tuser: string\n\tsystem: string\n\tname: string\n\tval: number\n\tcreated: string\n\tresolved?: string | null\n}\n\nexport interface QuietHoursRecord extends RecordModel {\n\tid: string\n\tuser: string\n\tsystem: string\n\ttype: \"one-time\" | \"daily\"\n\tstart: string\n\tend: string\n\texpand?: {\n\t\tsystem?: {\n\t\t\tname: string\n\t\t}\n\t}\n}\n\nexport interface ContainerRecord extends RecordModel {\n\tid: string\n\tsystem: string\n\tname: string\n\timage: string\n\tports: string\n\tcpu: number\n\tmemory: number\n\tnet: number\n\thealth: number\n\tstatus: string\n\tupdated: number\n}\n\nexport type ChartTimes = \"1m\" | \"1h\" | \"12h\" | \"24h\" | \"1w\" | \"30d\"\n\nexport interface ChartTimeData {\n\t[key: string]: {\n\t\ttype: \"1m\" | \"10m\" | \"20m\" | \"120m\" | \"480m\"\n\t\texpectedInterval: number\n\t\tlabel: () => string\n\t\tticks?: number\n\t\tformat: (timestamp: string) => string\n\t\tgetOffset: (endTime: Date) => Date\n\t\tminVersion?: string\n\t}\n}\n\nexport interface UserSettings {\n\tchartTime: ChartTimes\n\temails?: string[]\n\twebhooks?: string[]\n\tunitTemp?: Unit\n\tunitNet?: Unit\n\tunitDisk?: Unit\n\tcolorWarn?: number\n\tcolorCrit?: number\n\thourFormat?: HourFormat\n\tlayoutWidth?: number\n}\n\ntype ChartDataContainer = {\n\tcreated: number | null\n} & {\n\t[key: string]: key extends \"created\" ? never : ContainerStats\n}\n\nexport interface SemVer {\n\tmajor: number\n\tminor: number\n\tpatch: number\n}\n\nexport interface ChartData {\n\tagentVersion: SemVer\n\tsystemStats: SystemStatsRecord[]\n\tcontainerData: ChartDataContainer[]\n\torientation: \"right\" | \"left\"\n\tticks: number[]\n\tdomain: number[]\n\tchartTime: ChartTimes\n}\n\nexport interface AlertInfo {\n\tname: () => string\n\tunit: string\n\ticon: any\n\tdesc: () => string\n\tmax?: number\n\tmin?: number\n\tstep?: number\n\tstart?: number\n\t/** Single value description (when there's only one value, like status) */\n\tsingleDesc?: () => string\n\tinvert?: boolean\n}\n\nexport type AlertMap = Record<string, Map<string, AlertRecord>>\n\nexport interface SmartData {\n\t/** model family */\n\t// mf?: string\n\t/** model name */\n\tmn?: string\n\t/** serial number */\n\tsn?: string\n\t/** firmware version */\n\tfv?: string\n\t/** capacity */\n\tc?: number\n\t/** smart status */\n\ts?: string\n\t/** disk name (like /dev/sda) */\n\tdn?: string\n\t/** disk type */\n\tdt?: string\n\t/** temperature */\n\tt?: number\n\t/** attributes */\n\ta?: SmartAttribute[]\n}\n\nexport interface SmartAttribute {\n\t/** id */\n\tid?: number\n\t/** name */\n\tn: string\n\t/** value */\n\tv: number\n\t/** worst */\n\tw?: number\n\t/** threshold */\n\tt?: number\n\t/** raw value */\n\trv?: number\n\t/** raw string */\n\trs?: string\n\t/** when failed */\n\twf?: string\n}\n\nexport interface SystemDetailsRecord extends RecordModel {\n\tsystem: string\n\thostname: string\n\tkernel: string\n\tcores: number\n\tthreads: number\n\tcpu: string\n\tos: Os\n\tos_name: string\n\tmemory: number\n\tpodman: boolean\n}\n\nexport interface SmartDeviceRecord extends RecordModel {\n\tid: string\n\tsystem: string\n\tname: string\n\tmodel: string\n\tstate: string\n\tcapacity: number\n\ttemp: number\n\tfirmware: string\n\tserial: string\n\ttype: string\n\thours: number\n\tcycles: number\n\tattributes: SmartAttribute[]\n\tupdated: string\n}\n\nexport interface SystemdRecord extends RecordModel {\n\tsystem: string\n\tname: string\n\tstate: ServiceStatus\n\tsub: ServiceSubState\n\tcpu: number\n\tcpuPeak: number\n\tmemory: number\n\tmemPeak: number\n\tupdated: number\n}\n\nexport interface SystemdServiceDetails {\n\tAccessSELinuxContext: string;\n\tActivationDetails: any[];\n\tActiveEnterTimestamp: number;\n\tActiveEnterTimestampMonotonic: number;\n\tActiveExitTimestamp: number;\n\tActiveExitTimestampMonotonic: number;\n\tActiveState: string;\n\tAfter: string[];\n\tAllowIsolate: boolean;\n\tAssertResult: boolean;\n\tAssertTimestamp: number;\n\tAssertTimestampMonotonic: number;\n\tAsserts: any[];\n\tBefore: string[];\n\tBindsTo: any[];\n\tBoundBy: any[];\n\tCPUUsageNSec: number;\n\tCanClean: any[];\n\tCanFreeze: boolean;\n\tCanIsolate: boolean;\n\tCanLiveMount: boolean;\n\tCanReload: boolean;\n\tCanStart: boolean;\n\tCanStop: boolean;\n\tCollectMode: string;\n\tConditionResult: boolean;\n\tConditionTimestamp: number;\n\tConditionTimestampMonotonic: number;\n\tConditions: any[];\n\tConflictedBy: any[];\n\tConflicts: string[];\n\tConsistsOf: any[];\n\tDebugInvocation: boolean;\n\tDefaultDependencies: boolean;\n\tDescription: string;\n\tDocumentation: string[];\n\tDropInPaths: any[];\n\tExecMainPID: number;\n\tFailureAction: string;\n\tFailureActionExitStatus: number;\n\tFollowing: string;\n\tFragmentPath: string;\n\tFreezerState: string;\n\tId: string;\n\tIgnoreOnIsolate: boolean;\n\tInactiveEnterTimestamp: number;\n\tInactiveEnterTimestampMonotonic: number;\n\tInactiveExitTimestamp: number;\n\tInactiveExitTimestampMonotonic: number;\n\tInvocationID: string;\n\tJob: Array<number | string>;\n\tJobRunningTimeoutUSec: number;\n\tJobTimeoutAction: string;\n\tJobTimeoutRebootArgument: string;\n\tJobTimeoutUSec: number;\n\tJoinsNamespaceOf: any[];\n\tLoadError: string[];\n\tLoadState: string;\n\tMainPID: number;\n\tMarkers: any[];\n\tMemoryCurrent: number;\n\tMemoryLimit: number;\n\tMemoryPeak: number;\n\tNRestarts: number;\n\tNames: string[];\n\tNeedDaemonReload: boolean;\n\tOnFailure: any[];\n\tOnFailureJobMode: string;\n\tOnFailureOf: any[];\n\tOnSuccess: any[];\n\tOnSuccessJobMode: string;\n\tOnSuccessOf: any[];\n\tPartOf: any[];\n\tPerpetual: boolean;\n\tPropagatesReloadTo: any[];\n\tPropagatesStopTo: any[];\n\tRebootArgument: string;\n\tRefs: any[];\n\tRefuseManualStart: boolean;\n\tRefuseManualStop: boolean;\n\tReloadPropagatedFrom: any[];\n\tRequiredBy: any[];\n\tRequires: string[];\n\tRequiresMountsFor: any[];\n\tRequisite: any[];\n\tRequisiteOf: any[];\n\tResult: string;\n\tSliceOf: any[];\n\tSourcePath: string;\n\tStartLimitAction: string;\n\tStartLimitBurst: number;\n\tStartLimitIntervalUSec: number;\n\tStateChangeTimestamp: number;\n\tStateChangeTimestampMonotonic: number;\n\tStopPropagatedFrom: any[];\n\tStopWhenUnneeded: boolean;\n\tSubState: string;\n\tSuccessAction: string;\n\tSuccessActionExitStatus: number;\n\tSurviveFinalKillSignal: boolean;\n\tTasksCurrent: number;\n\tTasksMax: number;\n\tTransient: boolean;\n\tTriggeredBy: string[];\n\tTriggers: any[];\n\tUnitFilePreset: string;\n\tUnitFileState: string;\n\tUpheldBy: any[];\n\tUpholds: any[];\n\tWantedBy: any[];\n\tWants: string[];\n\tWantsMountsFor: any[];\n}"
  },
  {
    "path": "internal/site/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "internal/site/tsconfig.app.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"composite\": true,\n\t\t\"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n\t\t\"target\": \"ES2022\",\n\t\t\"useDefineForClassFields\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n\t\t\"skipLibCheck\": true,\n\t\t\"baseUrl\": \".\",\n\t\t\"paths\": {\n\t\t\t\"@/*\": [\"./src/*\"]\n\t\t},\n\n\t\t/* Bundler mode */\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowImportingTsExtensions\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"isolatedModules\": true,\n\t\t\"moduleDetection\": \"force\",\n\t\t\"noEmit\": true,\n\t\t\"jsx\": \"react-jsx\",\n\n\t\t/* Linting */\n\t\t\"strict\": true,\n\t\t\"noUnusedLocals\": true,\n\t\t\"noUnusedParameters\": true,\n\t\t\"noFallthroughCasesInSwitch\": true\n\t},\n\t\"include\": [\"src\"]\n}\n"
  },
  {
    "path": "internal/site/tsconfig.json",
    "content": "{\n\t\"files\": [],\n\t\"references\": [\n\t\t{\n\t\t\t\"path\": \"./tsconfig.app.json\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"./tsconfig.node.json\"\n\t\t}\n\t],\n\t\"compilerOptions\": {\n\t\t\"baseUrl\": \".\",\n\t\t\"paths\": {\n\t\t\t\"@/*\": [\"./src/*\"]\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/site/tsconfig.node.json",
    "content": "{\n\t\"compilerOptions\": {\n\t\t\"composite\": true,\n\t\t\"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n\t\t\"skipLibCheck\": true,\n\t\t\"module\": \"ESNext\",\n\t\t\"moduleResolution\": \"bundler\",\n\t\t\"allowSyntheticDefaultImports\": true,\n\t\t\"strict\": true,\n\t\t\"noEmit\": true\n\t},\n\t\"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "internal/site/vite.config.ts",
    "content": "import { defineConfig } from \"vite\"\nimport path from \"path\"\nimport tailwindcss from \"@tailwindcss/vite\"\nimport react from \"@vitejs/plugin-react-swc\"\nimport { lingui } from \"@lingui/vite-plugin\"\n\nexport default defineConfig({\n\tbase: \"./\",\n\tplugins: [\n\t\treact({\n\t\t\tplugins: [[\"@lingui/swc-plugin\", {}]],\n\t\t}),\n\t\tlingui(),\n\t\ttailwindcss(),\n\t],\n\tesbuild: {\n\t\tlegalComments: \"external\",\n\t},\n\tresolve: {\n\t\talias: {\n\t\t\t\"@\": path.resolve(__dirname, \"./src\"),\n\t\t},\n\t},\n})\n"
  },
  {
    "path": "internal/tests/api.go",
    "content": "package tests\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pocketbase/pocketbase/apis\"\n\t\"github.com/pocketbase/pocketbase/core\"\n\tpbtests \"github.com/pocketbase/pocketbase/tests\"\n\t\"github.com/pocketbase/pocketbase/tools/hook\"\n)\n\n// NOTE: This is a copy of https://github.com/pocketbase/pocketbase/blob/master/tests/api.go\n// with the following changes:\n// - Removed automatic cleanup of the test app in ApiScenario.Test (Aug 17 2025)\n\n// ApiScenario defines a single api request test case/scenario.\ntype ApiScenario struct {\n\t// Name is the test name.\n\tName string\n\n\t// Method is the HTTP method of the test request to use.\n\tMethod string\n\n\t// URL is the url/path of the endpoint you want to test.\n\tURL string\n\n\t// Body specifies the body to send with the request.\n\t//\n\t// For example:\n\t//\n\t//\tstrings.NewReader(`{\"title\":\"abc\"}`)\n\tBody io.Reader\n\n\t// Headers specifies the headers to send with the request (e.g. \"Authorization\": \"abc\")\n\tHeaders map[string]string\n\n\t// Delay adds a delay before checking the expectations usually\n\t// to ensure that all fired non-awaited go routines have finished\n\tDelay time.Duration\n\n\t// Timeout specifies how long to wait before cancelling the request context.\n\t//\n\t// A zero or negative value means that there will be no timeout.\n\tTimeout time.Duration\n\n\t// expectations\n\t// ---------------------------------------------------------------\n\n\t// ExpectedStatus specifies the expected response HTTP status code.\n\tExpectedStatus int\n\n\t// List of keywords that MUST exist in the response body.\n\t//\n\t// Either ExpectedContent or NotExpectedContent must be set if the response body is non-empty.\n\t// Leave both fields empty if you want to ensure that the response didn't have any body (e.g. 204).\n\tExpectedContent []string\n\n\t// List of keywords that MUST NOT exist in the response body.\n\t//\n\t// Either ExpectedContent or NotExpectedContent must be set if the response body is non-empty.\n\t// Leave both fields empty if you want to ensure that the response didn't have any body (e.g. 204).\n\tNotExpectedContent []string\n\n\t// List of hook events to check whether they were fired or not.\n\t//\n\t// You can use the wildcard \"*\" event key if you want to ensure\n\t// that no other hook events except those listed have been fired.\n\t//\n\t// For example:\n\t//\n\t//\tmap[string]int{ \"*\": 0 } // no hook events were fired\n\t//\tmap[string]int{ \"*\": 0, \"EventA\": 2 } // no hook events, except EventA were fired\n\t//\tmap[string]int{ \"EventA\": 2, \"EventB\": 0 } // ensures that EventA was fired exactly 2 times and EventB exactly 0 times.\n\tExpectedEvents map[string]int\n\n\t// test hooks\n\t// ---------------------------------------------------------------\n\n\tTestAppFactory func(t testing.TB) *pbtests.TestApp\n\tBeforeTestFunc func(t testing.TB, app *pbtests.TestApp, e *core.ServeEvent)\n\tAfterTestFunc  func(t testing.TB, app *pbtests.TestApp, res *http.Response)\n}\n\n// Test executes the test scenario.\n//\n// Example:\n//\n//\tfunc TestListExample(t *testing.T) {\n//\t    scenario := tests.ApiScenario{\n//\t        Name:           \"list example collection\",\n//\t        Method:         http.MethodGet,\n//\t        URL:            \"/api/collections/example/records\",\n//\t        ExpectedStatus: 200,\n//\t        ExpectedContent: []string{\n//\t            `\"totalItems\":3`,\n//\t            `\"id\":\"0yxhwia2amd8gec\"`,\n//\t            `\"id\":\"achvryl401bhse3\"`,\n//\t            `\"id\":\"llvuca81nly1qls\"`,\n//\t        },\n//\t        ExpectedEvents: map[string]int{\n//\t            \"OnRecordsListRequest\": 1,\n//\t            \"OnRecordEnrich\":       3,\n//\t        },\n//\t    }\n//\n//\t    scenario.Test(t)\n//\t}\nfunc (scenario *ApiScenario) Test(t *testing.T) {\n\tt.Run(scenario.normalizedName(), func(t *testing.T) {\n\t\tscenario.test(t)\n\t})\n}\n\n// Benchmark benchmarks the test scenario.\n//\n// Example:\n//\n//\tfunc BenchmarkListExample(b *testing.B) {\n//\t    scenario := tests.ApiScenario{\n//\t        Name:           \"list example collection\",\n//\t        Method:         http.MethodGet,\n//\t        URL:            \"/api/collections/example/records\",\n//\t        ExpectedStatus: 200,\n//\t        ExpectedContent: []string{\n//\t            `\"totalItems\":3`,\n//\t            `\"id\":\"0yxhwia2amd8gec\"`,\n//\t            `\"id\":\"achvryl401bhse3\"`,\n//\t            `\"id\":\"llvuca81nly1qls\"`,\n//\t        },\n//\t        ExpectedEvents: map[string]int{\n//\t            \"OnRecordsListRequest\": 1,\n//\t            \"OnRecordEnrich\":       3,\n//\t        },\n//\t    }\n//\n//\t    scenario.Benchmark(b)\n//\t}\nfunc (scenario *ApiScenario) Benchmark(b *testing.B) {\n\tb.Run(scenario.normalizedName(), func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tscenario.test(b)\n\t\t}\n\t})\n}\n\nfunc (scenario *ApiScenario) normalizedName() string {\n\tvar name = scenario.Name\n\n\tif name == \"\" {\n\t\tname = fmt.Sprintf(\"%s:%s\", scenario.Method, scenario.URL)\n\t}\n\n\treturn name\n}\n\nfunc (scenario *ApiScenario) test(t testing.TB) {\n\tvar testApp *pbtests.TestApp\n\tif scenario.TestAppFactory != nil {\n\t\ttestApp = scenario.TestAppFactory(t)\n\t\tif testApp == nil {\n\t\t\tt.Fatal(\"TestAppFactory must return a non-nill app instance\")\n\t\t}\n\t} else {\n\t\tvar testAppErr error\n\t\ttestApp, testAppErr = pbtests.NewTestApp()\n\t\tif testAppErr != nil {\n\t\t\tt.Fatalf(\"Failed to initialize the test app instance: %v\", testAppErr)\n\t\t}\n\t}\n\t// defer testApp.Cleanup()\n\n\tbaseRouter, err := apis.NewRouter(testApp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// manually trigger the serve event to ensure that custom app routes and middlewares are registered\n\tserveEvent := new(core.ServeEvent)\n\tserveEvent.App = testApp\n\tserveEvent.Router = baseRouter\n\n\tserveErr := testApp.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {\n\t\tif scenario.BeforeTestFunc != nil {\n\t\t\tscenario.BeforeTestFunc(t, testApp, e)\n\t\t}\n\n\t\t// reset the event counters in case a hook was triggered from a before func (eg. db save)\n\t\ttestApp.ResetEventCalls()\n\n\t\t// add middleware to timeout long-running requests (eg. keep-alive routes)\n\t\te.Router.Bind(&hook.Handler[*core.RequestEvent]{\n\t\t\tFunc: func(re *core.RequestEvent) error {\n\t\t\t\tslowTimer := time.AfterFunc(3*time.Second, func() {\n\t\t\t\t\tt.Logf(\"[WARN] Long running test %q\", scenario.Name)\n\t\t\t\t})\n\t\t\t\tdefer slowTimer.Stop()\n\n\t\t\t\tif scenario.Timeout > 0 {\n\t\t\t\t\tctx, cancelFunc := context.WithTimeout(re.Request.Context(), scenario.Timeout)\n\t\t\t\t\tdefer cancelFunc()\n\t\t\t\t\tre.Request = re.Request.Clone(ctx)\n\t\t\t\t}\n\n\t\t\t\treturn re.Next()\n\t\t\t},\n\t\t\tPriority: -9999,\n\t\t})\n\n\t\trecorder := httptest.NewRecorder()\n\n\t\treq := httptest.NewRequest(scenario.Method, scenario.URL, scenario.Body)\n\n\t\t// set default header\n\t\treq.Header.Set(\"content-type\", \"application/json\")\n\n\t\t// set scenario headers\n\t\tfor k, v := range scenario.Headers {\n\t\t\treq.Header.Set(k, v)\n\t\t}\n\n\t\t// execute request\n\t\tmux, err := e.Router.BuildMux()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Failed to build router mux: %v\", err)\n\t\t}\n\t\tmux.ServeHTTP(recorder, req)\n\n\t\tres := recorder.Result()\n\n\t\tif res.StatusCode != scenario.ExpectedStatus {\n\t\t\tt.Errorf(\"Expected status code %d, got %d\", scenario.ExpectedStatus, res.StatusCode)\n\t\t}\n\n\t\tif scenario.Delay > 0 {\n\t\t\ttime.Sleep(scenario.Delay)\n\t\t}\n\n\t\tif len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 {\n\t\t\tif len(recorder.Body.Bytes()) != 0 {\n\t\t\t\tt.Errorf(\"Expected empty body, got \\n%v\", recorder.Body.String())\n\t\t\t}\n\t\t} else {\n\t\t\t// normalize json response format\n\t\t\tbuffer := new(bytes.Buffer)\n\t\t\terr := json.Compact(buffer, recorder.Body.Bytes())\n\t\t\tvar normalizedBody string\n\t\t\tif err != nil {\n\t\t\t\t// not a json...\n\t\t\t\tnormalizedBody = recorder.Body.String()\n\t\t\t} else {\n\t\t\t\tnormalizedBody = buffer.String()\n\t\t\t}\n\n\t\t\tfor _, item := range scenario.ExpectedContent {\n\t\t\t\tif !strings.Contains(normalizedBody, item) {\n\t\t\t\t\tt.Errorf(\"Cannot find %v in response body \\n%v\", item, normalizedBody)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor _, item := range scenario.NotExpectedContent {\n\t\t\t\tif strings.Contains(normalizedBody, item) {\n\t\t\t\t\tt.Errorf(\"Didn't expect %v in response body \\n%v\", item, normalizedBody)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tremainingEvents := maps.Clone(testApp.EventCalls)\n\n\t\tvar noOtherEventsShouldRemain bool\n\t\tfor event, expectedNum := range scenario.ExpectedEvents {\n\t\t\tif event == \"*\" && expectedNum <= 0 {\n\t\t\t\tnoOtherEventsShouldRemain = true\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tactualNum := remainingEvents[event]\n\t\t\tif actualNum != expectedNum {\n\t\t\t\tt.Errorf(\"Expected event %s to be called %d, got %d\", event, expectedNum, actualNum)\n\t\t\t}\n\n\t\t\tdelete(remainingEvents, event)\n\t\t}\n\n\t\tif noOtherEventsShouldRemain && len(remainingEvents) > 0 {\n\t\t\tt.Errorf(\"Missing expected remaining events:\\n%#v\\nAll triggered app events are:\\n%#v\", remainingEvents, testApp.EventCalls)\n\t\t}\n\n\t\tif scenario.AfterTestFunc != nil {\n\t\t\tscenario.AfterTestFunc(t, testApp, res)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif serveErr != nil {\n\t\tt.Fatalf(\"Failed to trigger app serve hook: %v\", serveErr)\n\t}\n}\n"
  },
  {
    "path": "internal/tests/hub.go",
    "content": "//go:build testing\n\n// Package tests provides helpers for testing the application.\npackage tests\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/henrygd/beszel/internal/hub\"\n\n\t\"github.com/pocketbase/pocketbase/core\"\n\t\"github.com/pocketbase/pocketbase/tests\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t_ \"github.com/pocketbase/pocketbase/migrations\"\n)\n\n// TestHub is a wrapper hub instance used for testing.\ntype TestHub struct {\n\tcore.App\n\t*tests.TestApp\n\t*hub.Hub\n}\n\n// NewTestHub creates and initializes a test application instance.\n//\n// It is the caller's responsibility to call app.Cleanup() when the app is no longer needed.\nfunc NewTestHub(optTestDataDir ...string) (*TestHub, error) {\n\tvar testDataDir string\n\tif len(optTestDataDir) > 0 {\n\t\ttestDataDir = optTestDataDir[0]\n\t}\n\n\treturn NewTestHubWithConfig(core.BaseAppConfig{\n\t\tDataDir:       testDataDir,\n\t\tEncryptionEnv: \"pb_test_env\",\n\t})\n}\n\n// NewTestHubWithConfig creates and initializes a test application instance\n// from the provided config.\n//\n// If config.DataDir is not set it fallbacks to the default internal test data directory.\n//\n// config.DataDir is cloned for each new test application instance.\n//\n// It is the caller's responsibility to call app.Cleanup() when the app is no longer needed.\nfunc NewTestHubWithConfig(config core.BaseAppConfig) (*TestHub, error) {\n\ttestApp, err := tests.NewTestAppWithConfig(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thub := hub.NewHub(testApp)\n\n\tt := &TestHub{\n\t\tApp:     testApp,\n\t\tTestApp: testApp,\n\t\tHub:     hub,\n\t}\n\n\treturn t, nil\n}\n\n// Helper function to create a test user for config tests\nfunc CreateUser(app core.App, email string, password string) (*core.Record, error) {\n\tuserCollection, err := app.FindCachedCollectionByNameOrId(\"users\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser := core.NewRecord(userCollection)\n\tuser.Set(\"email\", email)\n\tuser.Set(\"password\", password)\n\n\treturn user, app.Save(user)\n}\n\n// Helper function to create a test record\nfunc CreateRecord(app core.App, collectionName string, fields map[string]any) (*core.Record, error) {\n\tcollection, err := app.FindCachedCollectionByNameOrId(collectionName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trecord := core.NewRecord(collection)\n\trecord.Load(fields)\n\n\treturn record, app.Save(record)\n}\n\nfunc ClearCollection(t testing.TB, app core.App, collectionName string) error {\n\t_, err := app.DB().NewQuery(fmt.Sprintf(\"DELETE from %s\", collectionName)).Execute()\n\trecordCount, err := app.CountRecords(collectionName)\n\tassert.EqualValues(t, recordCount, 0, \"should have 0 records after clearing\")\n\treturn err\n}\n\nfunc (h *TestHub) Cleanup() {\n\th.GetAlertManager().Stop()\n\th.GetSystemManager().RemoveAllSystems()\n\th.TestApp.Cleanup()\n}\n\nfunc CreateSystems(app core.App, count int, userId string, status string) ([]*core.Record, error) {\n\tsystems := make([]*core.Record, 0, count)\n\tfor i := range count {\n\t\tsystem, err := CreateRecord(app, \"systems\", map[string]any{\n\t\t\t\"name\":  fmt.Sprintf(\"test-system-%d\", i),\n\t\t\t\"host\":  fmt.Sprintf(\"127.0.0.%d\", i),\n\t\t\t\"port\":  \"33914\",\n\t\t\t\"users\": []string{userId},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsystem.Set(\"status\", status)\n\t\terr = app.SaveNoValidate(system)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsystems = append(systems, system)\n\t}\n\treturn systems, nil\n}\n\n// GetHubWithUser creates a test hub with a test user and user settings\nfunc GetHubWithUser(t *testing.T) (*TestHub, *core.Record) {\n\thub, err := NewTestHub(t.TempDir())\n\tassert.NoError(t, err)\n\thub.StartHub()\n\n\t// Manually initialize the system manager to bind event hooks\n\terr = hub.GetSystemManager().Initialize()\n\tassert.NoError(t, err)\n\n\t// Create a test user\n\tuser, err := CreateUser(hub, \"test@example.com\", \"password\")\n\tassert.NoError(t, err)\n\n\t// Create user settings for the test user (required for alert notifications)\n\tuserSettingsData := map[string]any{\n\t\t\"user\":     user.Id,\n\t\t\"settings\": `{\"emails\":[test@example.com],\"webhooks\":[]}`,\n\t}\n\t_, err = CreateRecord(hub, \"user_settings\", userSettingsData)\n\tassert.NoError(t, err)\n\n\treturn hub, user\n}\n"
  },
  {
    "path": "internal/users/users.go",
    "content": "// Package users handles user-related custom functionality.\npackage users\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/henrygd/beszel/internal/migrations\"\n\n\t\"github.com/pocketbase/dbx\"\n\t\"github.com/pocketbase/pocketbase/core\"\n)\n\ntype UserManager struct {\n\tapp core.App\n}\n\nfunc NewUserManager(app core.App) *UserManager {\n\treturn &UserManager{\n\t\tapp: app,\n\t}\n}\n\n// Initialize user role if not set\nfunc (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {\n\tif e.Record.GetString(\"role\") == \"\" {\n\t\te.Record.Set(\"role\", \"user\")\n\t}\n\treturn e.Next()\n}\n\n// Initialize user settings with defaults if not set\nfunc (um *UserManager) InitializeUserSettings(e *core.RecordEvent) error {\n\trecord := e.Record\n\t// intialize settings with defaults (zero values can be ignored)\n\tsettings := struct {\n\t\tChartTime string   `json:\"chartTime\"`\n\t\tEmails    []string `json:\"emails\"`\n\t}{\n\t\tChartTime: \"1h\",\n\t}\n\trecord.UnmarshalJSONField(\"settings\", &settings)\n\t// get user email from auth record\n\tvar user struct {\n\t\tEmail string `db:\"email\"`\n\t}\n\terr := e.App.DB().NewQuery(\"SELECT email FROM users WHERE id = {:id}\").Bind(dbx.Params{\n\t\t\"id\": record.GetString(\"user\"),\n\t}).One(&user)\n\tif err != nil {\n\t\tlog.Println(\"failed to get user email\", \"err\", err)\n\t\treturn err\n\t}\n\tsettings.Emails = []string{user.Email}\n\trecord.Set(\"settings\", settings)\n\treturn e.Next()\n}\n\n// Custom API endpoint to create the first user.\n// Mimics previous default behavior in PocketBase < 0.23.0 allowing user to be created through the Beszel UI.\nfunc (um *UserManager) CreateFirstUser(e *core.RequestEvent) error {\n\t// check that there are no users\n\ttotalUsers, err := um.app.CountRecords(\"users\")\n\tif err != nil || totalUsers > 0 {\n\t\treturn e.JSON(http.StatusForbidden, map[string]string{\"err\": \"Forbidden\"})\n\t}\n\t// check that there is only one superuser and the email matches the email of the superuser we set up in initial-settings.go\n\tadminUsers, err := um.app.FindAllRecords(core.CollectionNameSuperusers)\n\tif err != nil || len(adminUsers) != 1 || adminUsers[0].GetString(\"email\") != migrations.TempAdminEmail {\n\t\treturn e.JSON(http.StatusForbidden, map[string]string{\"err\": \"Forbidden\"})\n\t}\n\t// create first user using supplied email and password in request body\n\tdata := struct {\n\t\tEmail    string `json:\"email\"`\n\t\tPassword string `json:\"password\"`\n\t}{}\n\tif err := e.BindBody(&data); err != nil {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"err\": err.Error()})\n\t}\n\tif data.Email == \"\" || data.Password == \"\" {\n\t\treturn e.JSON(http.StatusBadRequest, map[string]string{\"err\": \"Bad request\"})\n\t}\n\n\tcollection, _ := um.app.FindCollectionByNameOrId(\"users\")\n\tuser := core.NewRecord(collection)\n\tuser.SetEmail(data.Email)\n\tuser.SetPassword(data.Password)\n\tuser.Set(\"role\", \"admin\")\n\tuser.Set(\"verified\", true)\n\tif err := um.app.Save(user); err != nil {\n\t\treturn e.JSON(http.StatusInternalServerError, map[string]string{\"err\": err.Error()})\n\t}\n\t// create superuser using the email of the first user\n\tcollection, _ = um.app.FindCollectionByNameOrId(core.CollectionNameSuperusers)\n\tadminUser := core.NewRecord(collection)\n\tadminUser.SetEmail(data.Email)\n\tadminUser.SetPassword(data.Password)\n\tif err := um.app.Save(adminUser); err != nil {\n\t\treturn e.JSON(http.StatusInternalServerError, map[string]string{\"err\": err.Error()})\n\t}\n\t// delete the intial superuser\n\tif err := um.app.Delete(adminUsers[0]); err != nil {\n\t\treturn e.JSON(http.StatusInternalServerError, map[string]string{\"err\": err.Error()})\n\t}\n\treturn e.JSON(http.StatusOK, map[string]string{\"msg\": \"User created\"})\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# Beszel\n\nBeszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions.\n\nIt 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.\n\n[![agent Docker Image Size](https://img.shields.io/docker/image-size/henrygd/beszel-agent/latest?logo=docker&label=agent%20image%20size)](https://hub.docker.com/r/henrygd/beszel-agent)\n[![hub Docker Image Size](https://img.shields.io/docker/image-size/henrygd/beszel/latest?logo=docker&label=hub%20image%20size)](https://hub.docker.com/r/henrygd/beszel)\n[![MIT license](https://img.shields.io/github/license/henrygd/beszel?color=%239944ee)](https://github.com/henrygd/beszel/blob/main/LICENSE)\n[![Crowdin](https://badges.crowdin.net/beszel/localized.svg)](https://crowdin.com/project/beszel)\n\n![Screenshot of Beszel dashboard and system page, side by side. The dashboard shows metrics from multiple connected systems, while the system page shows detailed metrics for a single system.](https://henrygd-assets.b-cdn.net/beszel/screenshot-new.png)\n\n## Features\n\n- **Lightweight**: Smaller and less resource-intensive than leading solutions.\n- **Simple**: Easy setup with little manual configuration required.\n- **Docker stats**: Tracks CPU, memory, and network usage history for each container.\n- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, load average, and status.\n- **Multi-user**: Users manage their own systems. Admins can share systems across users.\n- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.\n- **Automatic backups**: Save to and restore from disk or S3-compatible storage.\n<!-- - **REST API**: Use or update your data in your own scripts and applications. -->\n\n## Architecture\n\nBeszel consists of two main components: the **hub** and the **agent**.\n\n- **Hub**: A web application built on [PocketBase](https://pocketbase.io/) that provides a dashboard for viewing and managing connected systems.\n- **Agent**: Runs on each system you want to monitor and communicates system metrics to the hub.\n\n## Getting started\n\nThe [quick start guide](https://beszel.dev/guide/getting-started) and other documentation is available on our website, [beszel.dev](https://beszel.dev). You'll be up and running in a few minutes.\n\n## Screenshots\n\n![Dashboard](https://beszel.dev/image/dashboard.png)\n![System page](https://beszel.dev/image/system-full.png)\n![Notification Settings](https://beszel.dev/image/settings-notifications.png)\n\n## Supported metrics\n\n- **CPU usage** - Host system and Docker / Podman containers.\n- **Memory usage** - Host system and containers. Includes swap and ZFS ARC.\n- **Disk usage** - Host system. Supports multiple partitions and devices.\n- **Disk I/O** - Host system. Supports multiple partitions and devices.\n- **Network usage** - Host system and containers.\n- **Load average** - Host system.\n- **Temperature** - Host system sensors.\n- **GPU usage / power draw** - Nvidia, AMD, and Intel.\n- **Battery** - Host system battery charge.\n- **Containers** - Status and metrics of all running Docker / Podman containers.\n- **S.M.A.R.T.** - Host system disk health (includes eMMC wear/EOL and Linux mdraid array health via sysfs when available).\n\n## Help and discussion\n\nPlease search existing issues and discussions before opening a new one. I try my best to respond, but may not always have time to do so.\n\n#### Bug reports and feature requests\n\nBug reports and feature requests can be posted on [GitHub issues](https://github.com/henrygd/beszel/issues).\n\n#### Support and general discussion\n\nSupport requests and general discussion can be posted on [GitHub discussions](https://github.com/henrygd/beszel/discussions) or the community-run [Matrix room](https://matrix.to/#/#beszel:matrix.org): `#beszel:matrix.org`.\n\n## License\n\nBeszel is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.\n"
  },
  {
    "path": "supplemental/CHANGELOG.md",
    "content": "## 0.18.4\n\n- Add outbound heartbeat monitoring to external services (#1729)\n\n- Add experimental GPU monitoring for Apple Silicon. (#1747, #1746)\n\n- Add `nvtop` integration for GPU monitoring. (#1508)\n\n- Add `GPU_COLLECTOR` environment variable to manually specify the GPU collector(s).\n\n- Add eMMC health monitoring via sysfs. (#1736)\n\n- Add uptime to systems table. (#1719)\n\n- Add `DISABLE_SSH` environment variable to disable SSH agent functionality. (#1061)\n\n- Add `fingerprint` command to the agent. (#1726)\n\n- Add precise value entry for alerts via text input. (#1718)\n\n- Include GTT memory in AMD GPU metrics and improve device name lookup. (#1569)\n\n- Improve multiplexed logs detection for Podman. (#1755)\n\n- Harden against Docker API path traversal.\n\n- Fix issue where the agent could report incorrect root disk I/O when running in Docker. (#1737)\n\n- Retry Docker check on non-200 HTTP response. (#1754)\n\n- Fix race issue with meter threshold colors.\n\n- Update Go version and dependencies.\n\n- Add `InstallMethod` parameter to Windows install script.\n\n## 0.18.3\n\n- Add experimental sysfs AMD GPU collector. (#737, #1569)\n\n- Update LibreHardwareMonitorLib to 0.9.5. (#1697)\n\n- Improve container network stats accuracy.\n\n- Fix `SHARE_ALL_SYSTEMS` for system_details, smart_devices, and systemd_services. (#1660)\n\n- Parse ATA device statistics for temperature and future metrics. (#1689)\n\n- Add `SMART_DEVICES_SEPARATOR` environment variable and allow drives with the same name to be added with different types (e.g. RAID controllers). (#1655)\n\n- Add tooltips for navbar buttons. (#1636)\n\n- Add icon button for mobile use. (#1687)\n\n- Add tooltip to system name in systems table. (#1640)\n\n- Improve CJK truncation in UI.\n\n- Fix container uptime sorting edge case. (#1696)\n\n- Remove stale systemd services from tracking after deletion. (#1594)\n\n- Apply SELinux context after binary replacement. (#1678)\n\n- Update honeypot field name and autofill ignores. (#1011)\n\n- Write health_file to `/dev/shm` instead of `/tmp` if available. (#1455)\n\n- Don't force lowercase text for active alerts. (#1682)\n\n- Ensure battery current charge doesn't exceed full capacity. (#1668)\n\n- Increase `smartctl --scan` timeout to 10 seconds. (#1465)\n\n- Use name-only matching for unique S.M.A.R.T. devices. (#1655)\n\n- Fix smartctlArgs call to use hasExistingData flag. (#1645)\n\n- Ignore alt key combinations when navigating systems with arrow keys. (#1698)\n\n- Update Go dependencies\n\n## 0.18.2\n\n- Add separate dynamically linked glibc build for Linux. (#1618)\n\n- Fix GPU ID collision between Intel and NVIDIA collectors. (#1522)\n\n- Only hide GPU engine graph if entire usage is 0%. (#1624)\n\n- Add Jetson tegrastats regex support for pre-Jetpack 5 versions. (#1631)\n\n- Improve Indonesian translations. (#1625)\n\n## 0.18.1\n\n- Fix bug in 0.18.0 where all containers were cleared from the \"All Containers\" page when any system returned no containers.\n\n## 0.18.0\n\n- Add experimental NVML GPU collector. (#1522, #1587)\n\n- Add low battery alerts. (#1507)\n\n- Add battery charge to systems table.\n\n- Add option to make universal token permanent. (#1097, #1614)\n\n- Add `--url` and `--token` command line arguments to the agent. (#1524)\n\n- Collect S.M.A.R.T. data in the background every hour.\n\n- Add `SMART_INTERVAL` environment variable to customize S.M.A.R.T. data collection interval.\n\n- Collect system distribution and architecture.\n\n- Add `system_details` collection to store infrequently updated system information.\n\n- Improve S.M.A.R.T. device path lookup for NVMe devices. (#1504)\n\n- Use origin country flags for Spanish, Portuguese, English languages. (#1571)\n\n- Raise `smartctl` timeout to 15 seconds. (#1465)\n\n- Skip known non-unique product UUID when generating fingerprints. (#1556)\n\n- Fix container logs decoding for raw streams. (#1535)\n\n- Fix capacity sorting in S.M.A.R.T. table. (#1551)\n\n- Fix loader visibility when no systems are present. (#1511)\n\n- Rename login honeypot field to prevent password manager autofill (#1011).\n\n- Add Serbian and Bahasa Indonesia translations.\n\n- Update Go dependencies.\n\n## 0.17.0\n\n- Add quiet hours to silence alerts during specific time periods. (#265)\n\n- Add dedicated S.M.A.R.T. page.\n\n- Add alerts for S.M.A.R.T. failures.\n\n- Add `DISK_USAGE_CACHE` environment variable. (#1426)\n\n- Add `SKIP_SYSTEMD` environment variable. (#1448)\n\n- Add hub builds for Windows and FreeBSD.\n\n- Change extra disk indicators in systems table to display usage range as dots. (#1409)\n\n- Strip ANSI escape sequences from docker logs. (#1478)\n\n- Fix issue where the Add System button is visible to read-only users. (#1442)\n\n- Fix font ligatures creating unwanted artifacts in random ids. (#1434)\n\n- Update Go dependencies.\n\n## 0.16.1\n\n- Add services column to All Systems table. (#1153)\n\n- Add `SERVICE_PATTERNS` environment variable to filter systemd services. (#1153)\n\n- Fix detection and handling of immutable filesystems like Fedora Silverblue. (#1405)\n\n- Persist alert history page size preference. (#1404)\n\n- Add setting for layout width.\n\n- Update Go dependencies.\n\n## 0.16.0\n\n- Add basic systemd service monitoring. (#1153)\n\n- Add GPU usage alerts.\n\n- Show additional disk percentages in systems table. (#1365)\n\n- Embed `smartctl` in the Windows binary (experimental). (#1362)\n\n- Add `EXCLUDE_SMART` environment variable to exclude devices from S.M.A.R.T. monitoring. (#1392)\n\n- Change alert links to use system ID instead of name.\n\n- Update Go dependencies.\n\n## 0.15.4\n\n- Refactor containers table to fix clock issue causing no results. (#1337)\n\n- Fix Windows extra disk detection. (#1361)\n\n- Add total line to the tooltip of charts with multiple values. (#1280)\n\n- Add fallback paths for `smartctl` lookup. (#1362, #1363)\n\n- Fix `intel_gpu_top` parsing when engine instance id is in column. (#1230)\n\n- Update `henrygd/beszel-agent-nvidia` Dockerfile to build latest smartmontools. (#1335)\n\n## 0.15.3\n\n- Add CPU state details and per-core usage. (#1356)\n\n- Add `EXCLUDE_CONTAINERS` environment variable to exclude containers from being monitored. (#1352)\n\n- Add `INTEL_GPU_DEVICE` environment variable to specify Intel GPU device. (#1285)\n\n- Improve parsing of edge case S.M.A.R.T. power on times. (#1347)\n\n- Fix empty disk I/O values for extra disks. (#1355)\n\n- Fix battery nil pointer error. (#1353)\n\n- Add Hebrew with translations by @gabay.\n\n- Update `shoutrrr` and `gopsutil` dependencies.\n\n## 0.15.2\n\n- Improve S.M.A.R.T. device detection logic (fix regression in 0.15.1) (#1345)\n\n## 0.15.1\n\n- Add `SMART_DEVICES` environment variable to specify devices and types. (#373, #1335)\n\n- Add support for `scsi`, `sntasmedia`, and `sntrealtek` S.M.A.R.T. types. (#373, #1335)\n\n- Handle power-on time attributes that are formatted as strings (e.g., \"0h+0m+0.000s\").\n\n- Skip virtual disks in S.M.A.R.T. monitoring. (#1332)\n\n- Add sorting to the S.M.A.R.T. table. (#1333)\n\n- Fix incorrect disk rendering in S.M.A.R.T. device details. (#1336)\n\n- Fix `SHARE_ALL_SYSTEMS` setting not working for containers. (#1334)\n\n- Fix text contrast issue when container details are disabled. (#1324)\n\n## 0.15.0\n\n- Add initial S.M.A.R.T. support for disk health monitoring. (#962)\n\n- Add `henrygd/beszel-agent:alpine` Docker image and include `smartmontools` in all non-base agent images.\n\n- Remove environment variables from container details (#1305)\n\n- Add `CONTAINER_DETAILS` environment variable to control access to container logs and info APIs. (#1305)\n\n- Improve temperature chart by allowing y-axis to start above 0 for better readability. (#1307)\n\n- Improve battery detection logic. (#1287)\n\n- Limit docker log size to prevent possible memory leak. (#1322)\n\n- Update Go dependencies.\n\n## 0.14.1\n\n- Add `MFA_OTP` environment variable to enable email-based one-time password for users and/or superusers.\n\n- Add image name to containers table. (#1302)\n\n- Add spacing for long temperature chart tooltip. (#1299)\n\n- Fix sorting by status in containers table. (#1294)\n\n## 0.14.0\n\n- Add `/containers` page for viewing current status of all running containers. (#928)\n\n- Add ability to view container status, health, details, and basic logs. (#928)\n\n- Probable fix for erroneous network stats when interface resets (#1267, #1246)\n\n# 0.13.2\n\n- Add ability to set custom name for extra filesystems. (#379)\n\n- Improve WebSocket agent reconnection after network interruptions. (#1263)\n\n- Allow more latency in one minute charts before visually disconnecting points. (#1247)\n\n- Update favicon and add add down systems count in bubble.\n\n## 0.13.1\n\n- Fix one minute charts on systems without Docker. (#1237)\n\n- Change system permalinks to use ID instead of name. (#1231)\n\n## 0.13.0\n\n- Add one minute chart with one second interval.\n\n- Improve accuracy of disk I/O statistics.\n\n- Add `SYSTEM_NAME` environment variable to override system name on universal token registration. (#1184)\n\n- Add `noindex` HTML meta tag. (#1218)\n\n- Update Go dependencies.\n\n## 0.12.12\n\n- Fix high CPU usage when `intel_gpu_top` returns an error. (#1203)\n\n- Add `SKIP_GPU` environment variable to skip GPU data collection. (#1203)\n\n- Add fallback cache/buff memory calculation when cache/buff isn't available ([#1198](https://github.com/henrygd/beszel/issues/1198))\n\n- Fix automatic agent update / restart on OpenRC. (#1199)\n\n## 0.12.11\n\n- Adjust calculation of cached memory (fixes #1187, #1196)\n\n- Add pattern matching and blacklist functionality to `NICS` env var. (#1190)\n\n- Update Intel GPU collector to parse plain text (`-l`) instead of JSON output (#1150)\n\n## 0.12.10\n\nNote that the default memory calculation changed in this release, which may cause a difference in memory usage compared to previous versions.\n\n- Add initial support for Intel GPUs (#1150, #755)\n\n- Show connection type (WebSocket / SSH) in hub UI.\n\n- Fix temperature unit and bytes / bits settings. (#1180)\n\n- Add `henrygd/beszel-agent-intel` image for Intel GPUs (experimental).\n\n- Update Go dependencies. Shoutrrr now supports notifications for Signal and WeChat Work (WeCom).\n\n## 0.12.9\n\n- Fix divide by zero error introduced in 0.12.8 :) (#1175)\n\n## 0.12.8\n\n- Add per-interface network traffic charts. (#926)\n\n- Add cumulative network traffic charts. (#926)\n\n- Add setting for time format (12h / 24h). (#424)\n\n- Add experimental one-time password (OTP) support.\n\n- Add `TRUSTED_AUTH_HEADER` environment variable for authentication forwarding. (#399)\n\n- Add `AUTO_LOGIN` environment variable for automatic login. (#399)\n\n- Add FreeBSD support for agent install script and update command.\n\n- Fix status alerts not being resolved when system comes up. (#1052)\n\n## 0.12.7\n\n- Make LibreHardwareMonitor opt-in with `LHM=true` environment variable. (#1130)\n\n- Fix bug where token was not refreshed when adding a new system. (#1141)\n\n- Add `USER_EMAIL` and `USER_PASSWORD` environment variables to set the email and password of the initial user. (#1137)\n\n- Display system counts (active, paused, down) in All Systems 'view' options. (#1078)\n\n- Remember All Systems sort order during session.\n\n## 0.12.6\n\n- Add maximum 1 minute memory usage.\n\n- Add status filters to All Systems table.\n\n- Virtualize All Systems table to improve performance with hundreds of systems. (#1100)\n\n- Fix Safari system link CSS bug.\n\n- Use older cuda image for increased compatibility (#1103)\n\n- Truncate long system names in All Systems table. (#1104)\n\n- Fix update mirror and add `--china-mirrors` flag. (#1035)\n\n## 0.12.5\n\n- Downgrade `gopsutil` to `v4.25.6` to fix panic on FreeBSD (#1083)\n\n- Exclude FreeBSD from battery charge monitoring to fix deadlock. (#1081)\n\n- Minor hub UI improvements.\n\n## 0.12.4\n\n- Add battery charge monitoring.\n\n- Add fallback mirror to the `update` commands. (#1035)\n\n- Fix blank token field in insecure contexts.\n\n- Allow opening internal router links in new tab.\n\n- Add `/api/beszel/user-alerts` endpoint. Remove use of batch API for alerts in hub.\n\n- Require auth for `/api/beszel/getkey` endpoint that returns the public key.\n\n- Change `GET /api/beszel/send-test-notification` endpoint to `POST /api/beszel/test-notification`.\n\n- Update Go and JS dependencies.\n\n- New translations by @Radotornado, @AlexVanSteenhoven, @harupong, @dymek37, @NaNomicon, Tommaso Cavazza, Caio Garcia, and others.\n\n## Older\n\nRelease notes are available at <https://github.com/henrygd/beszel/releases>\n"
  },
  {
    "path": "supplemental/debian/beszel-agent.service",
    "content": "[Unit]\nDescription=Beszel Agent Service\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nEnvironment=\"PORT=45876\"\n# Port number can be overridden in beszel-agent.conf if needed\nEnvironmentFile=/etc/beszel-agent.conf\nExecStart=/usr/bin/beszel-agent\nUser=beszel\nRestart=on-failure\nStateDirectory=beszel-agent\n\n# Security/sandboxing settings\nKeyringMode=private\nLockPersonality=yes\nProtectClock=yes\nProtectHome=read-only\nProtectHostname=yes\nProtectKernelLogs=yes\nProtectSystem=strict\nRemoveIPC=yes\nRestrictSUIDSGID=true\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "supplemental/debian/config.sh",
    "content": "#!/bin/sh\nset -e\n\n. /usr/share/debconf/confmodule\ndb_version 2.0\n\ndb_input high beszel-agent/key || true\ndb_go\n"
  },
  {
    "path": "supplemental/debian/copyright",
    "content": "Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: Beszel\nUpstream-Contact: henrygd <hank@henrygd.me>\nSource: https://beszel.dev/\n\nFiles: *\nCopyright: 2024 henrygd\nLicense: MIT\n"
  },
  {
    "path": "supplemental/debian/lintian-overrides",
    "content": "# No changelog in the repo at the moment. This would be good to fix\nbeszel-agent: no-changelog\n# Current unable to fix these due to Goreleaser bug\n# https://github.com/goreleaser/goreleaser/issues/5487\nbeszel-agent: no-debconf-config\nbeszel-agent: postinst-uses-db-input\n# Needs to be fixed in Beszel build\nbeszel-agent: hardening-no-pie\nbeszel-agent: hardening-no-relro\n# Maybe one day\nbeszel-agent: no-manual-page\n"
  },
  {
    "path": "supplemental/debian/postinstall.sh",
    "content": "#!/bin/sh\nset -e\n\n[ \"$1\" = \"configure\" ] || exit 0\n\nCONFIG_FILE=/etc/beszel-agent.conf\nSERVICE=beszel-agent\nSERVICE_USER=beszel\n\n. /usr/share/debconf/confmodule\n\n# Create group and user\nif ! getent group \"$SERVICE_USER\" >/dev/null; then\n\techo \"Creating $SERVICE_USER group\"\n\taddgroup --quiet --system \"$SERVICE_USER\"\nfi\n\nif ! getent passwd \"$SERVICE_USER\" >/dev/null; then\n\techo \"Creating $SERVICE_USER user\"\n\tadduser --quiet --system \"$SERVICE_USER\" \\\n\t\t--ingroup \"$SERVICE_USER\" \\\n\t\t--no-create-home \\\n\t\t--home /nonexistent \\\n\t\t--gecos \"System user for $SERVICE\"\nfi\n\n# Enable docker (only if docker group exists)\nif getent group docker >/dev/null 2>&1; then\n\tif ! getent group docker | grep -q \"$SERVICE_USER\"; then\n\t\techo \"Adding $SERVICE_USER to docker group\"\n\t\tusermod -aG docker \"$SERVICE_USER\"\n\tfi\nfi\n\n# Create config file if it doesn't already exist\nif [ ! -f \"$CONFIG_FILE\" ]; then\n\ttouch \"$CONFIG_FILE\"\n\tchmod 0600 \"$CONFIG_FILE\"\n\tchown \"$SERVICE_USER\":\"$SERVICE_USER\" \"$CONFIG_FILE\"\nfi;\n\n# Only add key to config if it's not already present\nif ! grep -q \"^KEY=\" \"$CONFIG_FILE\"; then\n\tdb_get beszel-agent/key\n\techo \"KEY=$RET\" > \"$CONFIG_FILE\"\nfi;\n\ndeb-systemd-helper enable \"$SERVICE\".service\nsystemctl daemon-reload\ndeb-systemd-invoke start \"$SERVICE\".service || echo \"could not start $SERVICE.service!\"\n"
  },
  {
    "path": "supplemental/debian/postrm.sh",
    "content": "#!/bin/sh\nset -e\n\nif [ \"$1\" = \"purge\" ]; then\n\t. /usr/share/debconf/confmodule\n\tdb_purge\n\trm /etc/beszel-agent.conf\nfi\n"
  },
  {
    "path": "supplemental/debian/prerm.sh",
    "content": "#!/bin/sh\nset -e\n\nSERVICE=beszel-agent\n\ndeb-systemd-invoke stop \"$SERVICE\".service\nif [ \"$1\" = \"remove\" ]; then\n\tdeb-systemd-helper purge \"$SERVICE\".service\nfi\n"
  },
  {
    "path": "supplemental/debian/templates",
    "content": "Template: beszel-agent/key\nType: string\nDescription: SSH public key provided by beszel hub:\n If you leave this blank, you will need to configure it in \n /etc/beszel-agent.conf before starting Beszel.\n"
  },
  {
    "path": "supplemental/docker/agent/docker-compose.yml",
    "content": "services:\n  beszel-agent:\n    image: 'henrygd/beszel-agent' #Or henrygd/beszel-agent-nvidia\n    container_name: 'beszel-agent'\n    restart: unless-stopped\n    network_mode: host\n    # Only when using henrygd/beszel-agent-nvidia\n    # runtime: nvidia\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n      # monitor other disks / partitions by mounting a folder in /extra-filesystems\n      # - /mnt/disk/.beszel:/extra-filesystems/sda1:ro\n    environment:\n      PORT: 45876\n      KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'\n      # Only when using henrygd/beszel-agent-nvidia\n      # NVIDIA_VISIBLE_DEVICES: all \n      # NVIDIA_DRIVER_CAPABILITIES: compute,video,utility\n"
  },
  {
    "path": "supplemental/docker/hub/docker-compose.yml",
    "content": "services:\n  beszel:\n    image: 'henrygd/beszel'\n    container_name: 'beszel'\n    restart: unless-stopped\n    ports:\n      - '8090:8090'\n    volumes:\n      - ./beszel_data:/beszel_data\n"
  },
  {
    "path": "supplemental/docker/same-system/docker-compose.yml",
    "content": "services:\n  beszel:\n    image: 'henrygd/beszel'\n    container_name: 'beszel'\n    restart: unless-stopped\n    ports:\n      - '8090:8090'\n    volumes:\n      - ./beszel_data:/beszel_data\n    extra_hosts:\n      - 'host.docker.internal:host-gateway'\n\n  beszel-agent:\n    image: 'henrygd/beszel-agent' #Add -nvidia for nvidia gpus\n    container_name: 'beszel-agent'\n    restart: unless-stopped\n    network_mode: host\n    # runtime: nvidia # when using beszel-agent-nvidia\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:ro\n    environment:\n      PORT: 45876\n      KEY: '...'\n      # FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats\n      # NVIDIA_VISIBLE_DEVICES: all # when using beszel-agent-nvidia\n      # NVIDIA_DRIVER_CAPABILITIES: utility # when using beszel-agent-nvidia\n"
  },
  {
    "path": "supplemental/guides/systemd.md",
    "content": "# Installing as a Linux systemd service\n\nThis is useful if you want to run the hub or agent in the background continuously, including after a reboot.\n\n## Install script (recommended)\n\nThere are two scripts, one for the hub and one for the agent. You can run either one, or both.\n\nThe install script creates a dedicated user for the service (`beszel`), downloads the latest release, and installs the service.\n\nIf you need to edit the service -- for instance, to change an environment variable -- you can edit the file(s) in `/etc/systemd/system/`. Then reload the systemd daemon and restart the service.\n\n> [!NOTE]\n> You need system administrator privileges to run the install script. If you encounter a problem, please [open an issue](https://github.com/henrygd/beszel/issues/new).\n\n### Hub\n\nDownload the script:\n\n```bash\ncurl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-hub.sh -o install-hub.sh && chmod +x install-hub.sh\n```\n\n#### Install\n\nYou may specify a port number with the `-p` flag. The default port is `8090`.\n\n```bash\n./install-hub.sh\n```\n\n#### Uninstall\n\n```bash\n./install-hub.sh -u\n```\n\n#### Update\n\n```bash\nsudo /opt/beszel/beszel update && sudo systemctl restart beszel-hub\n```\n\n### Agent\n\nDownload the script:\n\n```bash\ncurl -sL https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/install-agent.sh -o install-agent.sh && chmod +x install-agent.sh\n```\n\n#### Install\n\nYou may optionally include the SSH key and port as arguments. Run `./install-agent.sh -h` for more info.\n\nIf specifying your key with `-k`, please make sure to enclose it in quotes.\n\n```bash\n./install-agent.sh\n```\n\n#### Uninstall\n\n```bash\n./install-agent.sh -u\n```\n\n#### Update\n\n```bash\nsudo /opt/beszel-agent/beszel-agent update && sudo systemctl restart beszel-agent\n```\n\n## Manual install\n\n### Hub\n\n1. Create the system service at `/etc/systemd/system/beszel.service`\n\n```bash\n[Unit]\nDescription=Beszel Hub Service\nAfter=network.target\n\n[Service]\n# update the values in the curly braces below (remove the braces)\nExecStart={/path/to/working/directory}/beszel serve\nWorkingDirectory={/path/to/working/directory}\nUser={YOUR_USERNAME}\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n```\n\n2. Start and enable the service to let it run after system boot\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable beszel.service\nsudo systemctl start beszel.service\n```\n\n### Agent\n\n1. Create the system service at `/etc/systemd/system/beszel-agent.service`\n\n```bash\n[Unit]\nDescription=Beszel Agent Service\nAfter=network.target\n\n[Service]\n# update the values in curly braces below (remove the braces)\nEnvironment=\"PORT={PASTE_YOUR_PORT_HERE}\"\nEnvironment=\"KEY={PASTE_YOUR_KEY_HERE}\"\n# Environment=\"EXTRA_FILESYSTEMS={sdb}\"\nExecStart={/path/to/directory}/beszel-agent\nUser={YOUR_USERNAME}\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n```\n\n2. Start and enable the service to let it run after system boot\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable beszel-agent.service\nsudo systemctl start beszel-agent.service\n```\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/Chart.yaml",
    "content": "apiVersion: v1\ndescription: Installs beszel-hub in kubernetes\nhome: https://github.com/dnikoloski/beszel-kubernetes/tree/main/charts/beszel-hub\nname: beszel-hub\nappVersion: \"0.9\"\n# Do not touch will be updated during release\nversion: 0.1.0\nsources:\n  - https://github.com/dnikoloski/beszel-kubernetes/tree/main/charts/beszel-hub\n  - https://www.beszel.dev/\n  - https://github.com/henrygd/beszel\nicon: https://repository-images.githubusercontent.com/825470378/2710c6db-f934-4a8b-a2c4-7a0abbcd2ad6\nmaintainers:\n  - name: dnikoloski\n    email: nikoloskid@pm.me\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/NOTES.txt",
    "content": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n{{- else if contains \"NodePort\" .Values.service.type }}\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"beszel.fullname\" . }}-web)\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n     NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n           You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"beszel.fullname\" . }}'\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"beszel.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l \"app.kubernetes.io/name={{ include \"beszel.name\" . }},app.kubernetes.io/instance={{ .Release.Name }}\" -o jsonpath=\"{.items[0].metadata.name}\")\n  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n  echo \"Visit http://127.0.0.1:8090 to use your application\"\n  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8090:$CONTAINER_PORT\n{{- end }}\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"beszel.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"beszel.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"beszel.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"beszel.labels\" -}}\nhelm.sh/chart: {{ include \"beszel.chart\" . }}\n{{ include \"beszel.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"beszel.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"beszel.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"beszel.fullname\" . }}\n  labels:\n    {{- include \"beszel.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  strategy:\n    type: {{ .Values.strategyType }}\n    {{- if eq .Values.strategyType \"RollingUpdate\" }}\n    rollingUpdate:\n      maxSurge: {{ .Values.maxSurge }}\n      maxUnavailable: {{ .Values.maxUnavailable }}\n    {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"beszel.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"beszel.labels\" . | nindent 8 }}\n        {{- with .Values.podLabels }}\n        {{- toYaml . | nindent 8 }}\n        {{- end }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.podSecurityContext }}\n      securityContext:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      hostname: {{ .Values.hostname }}\n      hostNetwork: {{ .Values.hostNetwork }}\n      containers:\n        - name: {{ .Chart.Name }}\n          {{- with .Values.securityContext }}\n          securityContext:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.port }}\n              protocol: TCP\n          {{- with .Values.livenessProbe }}\n          livenessProbe:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          {{- with .Values.readinessProbe }}\n          readinessProbe:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          {{- with .Values.resources }}\n          resources:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          {{- if .Values.persistentVolumeClaim.enabled }}\n          volumeMounts:\n            - name: data\n              mountPath: /beszel_data\n            {{- with .Values.volumeMounts }}\n              {{- toYaml . | nindent 12 }}\n            {{- end }}\n          {{- else if .Values.volumeMounts }}\n          volumeMounts:\n            {{- toYaml .Values.volumeMounts | nindent 12 }}\n          {{- end }}\n      {{- if .Values.persistentVolumeClaim.enabled }}\n      volumes:\n        - name: data\n          persistentVolumeClaim:\n            claimName: {{ .Values.persistentVolumeClaim.existingClaim | default (include \"beszel.fullname\" .) }}\n        {{- with .Values.volumes }}\n          {{- toYaml . | nindent 8 }}\n        {{- end }}\n      {{- else if .Values.volumes }}\n      volumes:\n        {{- toYaml .Values.volumes | nindent 8 }}\n      {{- end }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: {{ include \"beszel.fullname\" . }}\n  labels:\n    {{- include \"beszel.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- with .Values.ingress.className }}\n  ingressClassName: {{ . }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            {{- with .pathType }}\n            pathType: {{ . }}\n            {{- end }}\n            backend:\n              service:\n                name: {{ include \"beszel.fullname\" $ }}\n                port:\n                  number: {{ $.Values.service.port }}\n          {{- end }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"beszel.fullname\" . }}\n  labels:\n    {{- include \"beszel.labels\" . | nindent 4 }}\n{{- if .Values.service.annotations }}\n  annotations:\n{{ toYaml .Values.service.annotations | indent 4 }}\n{{- end }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  {{- if .Values.service.loadBalancerIP }}\n  loadBalancerIP: {{ .Values.service.loadBalancerIP }}\n  {{- end }}\n  selector:\n    {{- include \"beszel.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/tests/test-beszel-hub-endpoint.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ .Release.Name }}-smoke-test\"\n  annotations:\n    \"helm.sh/hook\": test\nspec:\n  containers:\n  - name: hook1-container\n    image: curlimages/curl\n    imagePullPolicy: IfNotPresent\n    command: ['sh', '-c', 'curl http://{{ template \"beszel.fullname\" . }}-web:8090/']\n  restartPolicy: Never\n  terminationGracePeriodSeconds: 0"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/templates/volume-claim.yaml",
    "content": "{{- if .Values.persistentVolumeClaim.enabled -}}\n{{- if not .Values.persistentVolumeClaim.existingClaim -}}\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n{{- if .Values.persistentVolumeClaim.annotations }}\n  annotations:\n{{ toYaml .Values.persistentVolumeClaim.annotations | indent 4 }}\n{{- end }}\n  labels:\n    {{- include \"beszel.labels\" . | nindent 4 }}\n  name: {{ template \"beszel.fullname\" . }}\nspec:\n  accessModes:\n{{ toYaml .Values.persistentVolumeClaim.accessModes | indent 4 }}\n{{- if .Values.persistentVolumeClaim.storageClass }}\n  {{- if (eq \"-\" .Values.persistentVolumeClaim.storageClass) }}\n  storageClassName: \"\"\n  {{- else }}\n  storageClassName: {{ .Values.persistentVolumeClaim.storageClass | quote }}\n  {{- end }}\n{{- end }}\n  resources:\n    requests:\n      storage: {{ .Values.persistentVolumeClaim.size | quote }}\n{{- end -}}\n{{- end -}}"
  },
  {
    "path": "supplemental/kubernetes/beszel-hub/charts/values.yaml",
    "content": "# Default values for beszel-hub.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\n# -- The number of replicas\nreplicaCount: 1\n\nimage:\n  repository: henrygd/beszel\n  pullPolicy: IfNotPresent\n  tag: \"\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\n\npodAnnotations: {}\npodLabels: {}\n\npodSecurityContext: {}\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  enabled: true\n  annotations: {}\n  type: ClusterIP\n  loadBalancerIP: \"\"\n  port: 8090\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: chart-example.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n  #  - secretName: chart-example-tls\n  #    hosts:\n  #      - chart-example.local\n\nresources: {}\n  # limits:\n  #  cpu: 100m\n  #  memory: 128Mi\n  # requests:\n  #  cpu: 100m\n  #  memory: 128Mi\n\nlivenessProbe:\n  httpGet:\n    path: /\n    port: http\nreadinessProbe:\n  httpGet:\n    path: /\n    port: http\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 100\n  targetCPUUtilizationPercentage: 80\n\n# volumes: {}\n\n# volumeMounts: {}\n\n# -- `spec.PersitentVolumeClaim` configuration\npersistentVolumeClaim:\n  # -- set to true to use pvc\n  enabled: true\n\n  # -- specify an existing `PersistentVolumeClaim` to use\n  # existingClaim: \"\"\n\n  # -- Annotations for the `PersitentVolumeClaim`\n  annotations: {}\n\n  accessModes:\n    - ReadWriteOnce\n\n  storageClass: \"\"\n\n  # -- volume claim size\n  size: \"500Mi\"\n\n# -- hostname of pod\nhostname: \"\"\n\n# -- should the container use host network\nhostNetwork: \"false\"\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n"
  },
  {
    "path": "supplemental/licenses/LibreHardwareMonitor/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0."
  },
  {
    "path": "supplemental/licenses/smartmontools/LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License."
  },
  {
    "path": "supplemental/scripts/install-agent-brew.sh",
    "content": "#!/bin/bash\n\nPORT=45876\nKEY=\"\"\nTOKEN=\"\"\nHUB_URL=\"\"\n\nusage() {\n  printf \"Beszel Agent homebrew installation script\\n\\n\"\n  printf \"Usage: ./install-agent-brew.sh [options]\\n\\n\"\n  printf \"Options: \\n\"\n  printf \"  -k            SSH key (required, or interactive if not provided)\\n\"\n  printf \"  -p            Port (default: $PORT)\\n\"\n  printf \"  -t            Token (optional for backwards compatibility)\\n\"\n  printf \"  -url          Hub URL (optional for backwards compatibility)\\n\"\n  printf \"  -h, --help    Display this help message\\n\"\n  exit 0\n}\n\n# Parse arguments\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n  -k)\n    shift\n    KEY=\"$1\"\n    ;;\n  -p)\n    shift\n    PORT=\"$1\"\n    ;;\n  -t)\n    shift\n    TOKEN=\"$1\"\n    ;;\n  -url)\n    shift\n    HUB_URL=\"$1\"\n    ;;\n  -h | --help)\n    usage\n    ;;\n  *)\n    echo \"Invalid option: $1\" >&2\n    usage\n    ;;\n  esac\n  shift\ndone\n\n# Check if brew is installed, prompt to install if not\nif ! command -v brew &>/dev/null; then\n  read -p \"Homebrew is not installed. Would you like to install it now? (y/n): \" install_brew\n  if [[ $install_brew =~ ^[Yy]$ ]]; then\n    echo \"Installing Homebrew...\"\n    /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\n    # Verify installation was successful\n    if ! command -v brew &>/dev/null; then\n      echo \"Homebrew installation failed. Please install manually and try again.\"\n      exit 1\n    fi\n    echo \"Homebrew installed successfully.\"\n  else\n    echo \"Homebrew is required. Please install Homebrew and try again.\"\n    exit 1\n  fi\nfi\n\nif [ -z \"$KEY\" ]; then\n  read -p \"Enter SSH key: \" KEY\nfi\n\n# TOKEN and HUB_URL are optional for backwards compatibility - no interactive prompts\n\nmkdir -p ~/.config/beszel ~/.cache/beszel\n\necho \"KEY=\\\"$KEY\\\"\" >~/.config/beszel/beszel-agent.env\necho \"LISTEN=$PORT\" >>~/.config/beszel/beszel-agent.env\n\nif [ -n \"$TOKEN\" ]; then\n  echo \"TOKEN=\\\"$TOKEN\\\"\" >>~/.config/beszel/beszel-agent.env\nfi\nif [ -n \"$HUB_URL\" ]; then\n  echo \"HUB_URL=\\\"$HUB_URL\\\"\" >>~/.config/beszel/beszel-agent.env\nfi\n\nbrew tap henrygd/beszel\nbrew install beszel-agent\nbrew services start beszel-agent\n\nprintf \"\\nCheck status: brew services info beszel-agent\\n\"\necho \"Stop: brew services stop beszel-agent\"\necho \"Start: brew services start beszel-agent\"\necho \"Restart: brew services restart beszel-agent\"\necho \"Upgrade: brew upgrade beszel-agent\"\necho \"Uninstall: brew uninstall beszel-agent\"\necho \"View logs in ~/.cache/beszel/beszel-agent.log\"\nprintf \"Change environment variables in ~/.config/beszel/beszel-agent.env\\n\"\n"
  },
  {
    "path": "supplemental/scripts/install-agent.ps1",
    "content": "param (\n    [switch]$Elevated,\n    [Parameter(Mandatory=$true)]\n    [string]$Key,\n    [string]$Token = \"\",\n    [string]$Url = \"\",\n    [int]$Port = 45876,\n    [string]$AgentPath = \"\",\n    [string]$NSSMPath = \"\",\n    [switch]$ConfigureFirewall,\n    [ValidateSet(\"Auto\", \"Scoop\", \"WinGet\")]\n    [string]$InstallMethod = \"Auto\"\n)\n\n# Check if required parameters are provided\nif ([string]::IsNullOrWhiteSpace($Key)) {\n    Write-Host \"ERROR: SSH Key is required.\" -ForegroundColor Red\n    Write-Host \"Usage: .\\install-agent.ps1 -Key 'your-ssh-key-here' [-Token 'your-token-here'] [-Url 'your-hub-url-here'] [-Port port-number] [-InstallMethod Auto|Scoop|WinGet] [-ConfigureFirewall]\" -ForegroundColor Yellow\n    Write-Host \"Note: Token and Url are optional for backwards compatibility with older hub versions.\" -ForegroundColor Yellow\n    exit 1\n}\n\n# Stop on first error\n$ErrorActionPreference = \"Stop\"\n\n#region Utility Functions\n\n# Function to check if running as admin\nfunction Test-Admin {\n    return ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)\n}\n\n# Function to check if a command exists\nfunction Test-CommandExists {\n    param (\n        [Parameter(Mandatory=$true)]\n        [string]$Command\n    )\n    return (Get-Command $Command -ErrorAction SilentlyContinue)\n}\n\n# Function to find beszel-agent in common installation locations\nfunction Find-BeszelAgent {\n    # First check if it's in PATH\n    $agentCmd = Get-Command \"beszel-agent\" -ErrorAction SilentlyContinue\n    if ($agentCmd) {\n        return $agentCmd.Source\n    }\n    \n    # Common installation paths to check\n    $commonPaths = @(\n        \"$env:USERPROFILE\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\",\n        \"$env:ProgramData\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\",\n        \"$env:LOCALAPPDATA\\Microsoft\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"$env:ProgramFiles\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"${env:ProgramFiles(x86)}\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"$env:ProgramFiles\\beszel-agent\\beszel-agent.exe\",\n        \"$env:ProgramFiles(x86)\\beszel-agent\\beszel-agent.exe\",\n        \"$env:SystemDrive\\Users\\*\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\"\n    )\n    \n    foreach ($path in $commonPaths) {\n        # Handle wildcard paths\n        if ($path.Contains(\"*\")) {\n            $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue\n            if ($foundPaths) {\n                return $foundPaths[0].FullName\n            }\n        } else {\n            if (Test-Path $path) {\n                return $path\n            }\n        }\n    }\n    \n    return $null\n}\n\n# Function to find NSSM in common installation locations\nfunction Find-NSSM {\n    # First check if it's in PATH\n    $nssmCmd = Get-Command \"nssm\" -ErrorAction SilentlyContinue\n    if ($nssmCmd) {\n        return $nssmCmd.Source\n    }\n    \n    # Common installation paths to check\n    $commonPaths = @(\n        \"$env:USERPROFILE\\scoop\\apps\\nssm\\current\\nssm.exe\",\n        \"$env:ProgramData\\scoop\\apps\\nssm\\current\\nssm.exe\",\n        \"$env:LOCALAPPDATA\\Microsoft\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"$env:ProgramFiles\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"${env:ProgramFiles(x86)}\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"$env:SystemDrive\\Users\\*\\scoop\\apps\\nssm\\current\\nssm.exe\"\n    )\n    \n    foreach ($path in $commonPaths) {\n        # Handle wildcard paths\n        if ($path.Contains(\"*\")) {\n            $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue\n            if ($foundPaths) {\n                return $foundPaths[0].FullName\n            }\n        } else {\n            if (Test-Path $path) {\n                return $path\n            }\n        }\n    }\n    \n    return $null\n}\n\n#endregion\n\n#region Installation Methods\n\n# Function to install Scoop\nfunction Install-Scoop {\n    Write-Host \"Installing Scoop...\"\n    \n    # Check if running as admin - Scoop should not be installed as admin\n    if (Test-Admin) {\n        throw \"Scoop cannot be installed with administrator privileges. Please run this script as a regular user first to install Scoop and beszel-agent, then run as admin to configure the service.\"\n    }\n    \n    try {\n        Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression\n        \n        if (-not (Test-CommandExists \"scoop\")) {\n            throw \"Failed to install Scoop - command not available after installation\"\n        }\n        Write-Host \"Scoop installed successfully.\"\n    }\n    catch {\n        throw \"Failed to install Scoop: $($_.Exception.Message)\"\n    }\n}\n\n# Function to install Git via Scoop\nfunction Install-Git {\n    if (Test-CommandExists \"git\") {\n        Write-Host \"Git is already installed.\"\n        return\n    }\n    \n    Write-Host \"Installing Git...\"\n    scoop install git\n    \n    if (-not (Test-CommandExists \"git\")) {\n        throw \"Failed to install Git\"\n    }\n}\n\n# Function to install NSSM\nfunction Install-NSSM {\n    param (\n        [string]$Method = \"Scoop\" # Default to Scoop method\n    )\n    \n    if (Test-CommandExists \"nssm\") {\n        Write-Host \"NSSM is already installed.\"\n        return\n    }\n    \n    Write-Host \"Installing NSSM...\"\n    if ($Method -eq \"Scoop\") {\n        scoop install nssm\n    }\n    elseif ($Method -eq \"WinGet\") {\n        winget install -e --id NSSM.NSSM --accept-source-agreements --accept-package-agreements\n        \n        # Refresh PATH environment variable to make NSSM available in current session\n        $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\", \"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\", \"User\")\n    }\n    else {\n        throw \"Unsupported installation method: $Method\"\n    }\n    \n    if (-not (Test-CommandExists \"nssm\")) {\n        throw \"Failed to install NSSM\"\n    }\n}\n\n# Function to install beszel-agent with Scoop\nfunction Install-BeszelAgentWithScoop {\n    Write-Host \"Adding beszel bucket...\"\n    scoop bucket add beszel https://github.com/henrygd/beszel-scoops | Out-Null\n    \n    Write-Host \"Installing / updating beszel-agent...\"\n    scoop install beszel-agent | Out-Null\n    \n    if (-not (Test-CommandExists \"beszel-agent\")) {\n        throw \"Failed to install beszel-agent\"\n    }\n    \n    return $(Join-Path -Path $(scoop prefix beszel-agent) -ChildPath \"beszel-agent.exe\")\n}\n\n# Function to install beszel-agent with WinGet\nfunction Install-BeszelAgentWithWinGet {\n    Write-Host \"Installing / updating beszel-agent...\"\n    \n    # Temporarily change ErrorActionPreference to allow WinGet to complete and show output\n    $originalErrorActionPreference = $ErrorActionPreference\n    $ErrorActionPreference = \"Continue\"\n    \n    # Use call operator (&) and capture exit code properly\n    & winget install --exact --id henrygd.beszel-agent --accept-source-agreements --accept-package-agreements | Out-Null\n    $wingetExitCode = $LASTEXITCODE\n    \n    # Restore original ErrorActionPreference\n    $ErrorActionPreference = $originalErrorActionPreference\n    \n    # WinGet exit codes:\n    # 0 = Success\n    # -1978335212 (0x8A150014) = No applicable upgrade found (package is up to date)\n    # -1978335189 (0x8A15002B) = Another \"no upgrade needed\" variant\n    # Other codes indicate actual errors\n    if ($wingetExitCode -eq -1978335212 -or $wingetExitCode -eq -1978335189) {\n        Write-Host \"Package is already up to date.\" -ForegroundColor Green\n    } elseif ($wingetExitCode -ne 0)  {\n        Write-Host \"WinGet exit code: $wingetExitCode\" -ForegroundColor Yellow\n    }\n    \n    # Refresh PATH environment variable to make beszel-agent available in current session\n    $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\", \"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\", \"User\")\n    \n    # Find the path to the beszel-agent executable\n    $agentPath = (Get-Command beszel-agent -ErrorAction SilentlyContinue).Source\n    \n    if (-not $agentPath) {\n        throw \"Could not find beszel-agent executable path after installation\"\n    }\n    \n    return $agentPath\n}\n\n# Function to install using Scoop\nfunction Install-WithScoop {\n    param (\n        [string]$Key,\n        [int]$Port\n    )\n    \n    try {\n        # Ensure Scoop is installed\n        if (-not (Test-CommandExists \"scoop\")) {\n            Install-Scoop | Out-Null\n        }\n        else {\n            Write-Host \"Scoop is already installed.\"\n        }\n        \n        # Install Git (required for Scoop buckets)\n        Install-Git | Out-Null\n        \n        # Install NSSM\n        Install-NSSM -Method \"Scoop\" | Out-Null\n        \n        # Install beszel-agent\n        $agentPath = Install-BeszelAgentWithScoop\n        \n        return $agentPath\n    }\n    catch {\n        Write-Host \"ERROR: $($_.Exception.Message)\" -ForegroundColor Red\n        Write-Host \"Installation failed. Please check the error message above.\" -ForegroundColor Red\n        Write-Host \"Press any key to exit...\" -ForegroundColor Red\n        $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n        exit 1\n    }\n}\n\n# Function to install using WinGet\nfunction Install-WithWinGet {\n    param (\n        [string]$Key,\n        [int]$Port\n    )\n    \n    try {\n        # Install NSSM\n        Install-NSSM -Method \"WinGet\" | Out-Null\n        \n        # Install beszel-agent\n        $agentPath = Install-BeszelAgentWithWinGet\n        \n        return $agentPath\n    }\n    catch {\n        Write-Host \"ERROR: $($_.Exception.Message)\" -ForegroundColor Red\n        Write-Host \"Installation failed. Please check the error message above.\" -ForegroundColor Red\n        Write-Host \"Press any key to exit...\" -ForegroundColor Red\n        $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n        exit 1\n    }\n}\n\n#endregion\n\n#region Service Configuration\n\n# Function to install and configure the NSSM service\nfunction Install-NSSMService {\n    param (\n        [Parameter(Mandatory=$true)]\n        [string]$AgentPath,\n        [Parameter(Mandatory=$true)]\n        [string]$Key,\n        [string]$Token = \"\",\n        [string]$HubUrl = \"\",\n        [Parameter(Mandatory=$true)]\n        [int]$Port,\n        [string]$NSSMPath = \"\"\n    )\n    \n    Write-Host \"Installing beszel-agent service...\"\n    \n    # Determine the NSSM executable to use\n    $nssmCommand = \"nssm\"\n    if ($NSSMPath -and (Test-Path $NSSMPath)) {\n        $nssmCommand = $NSSMPath\n        Write-Host \"Using NSSM from: $NSSMPath\"\n    } elseif (-not (Test-CommandExists \"nssm\")) {\n        throw \"NSSM is not available in PATH and no valid NSSMPath was provided\"\n    }\n    \n    # Check if service already exists\n    $existingService = Get-Service -Name \"beszel-agent\" -ErrorAction SilentlyContinue\n    if ($existingService) {\n        Write-Host \"Service already exists. Checking if path update is needed...\"\n        \n        # Get current service path \n        try {\n            $currentPath = & $nssmCommand get beszel-agent Application\n            if ($LASTEXITCODE -eq 0 -and $currentPath.Trim() -eq $AgentPath) {\n                Write-Host \"Service already configured with correct path. Skipping service recreation.\" -ForegroundColor Green\n                return\n            }\n            \n            Write-Host \"Service path needs updating. Stopping and removing existing service...\"\n            Write-Host \"  Current path: $($currentPath.Trim())\"\n            Write-Host \"  New path: $AgentPath\"\n        } catch {\n            Write-Host \"Could not retrieve current service path, will recreate service: $($_.Exception.Message)\" -ForegroundColor Yellow\n            Write-Host \"Service path needs updating. Stopping and removing existing service...\"\n        }\n        \n        try {\n            & $nssmCommand stop beszel-agent\n            & $nssmCommand remove beszel-agent confirm\n        } catch {\n            Write-Host \"Warning: Failed to remove existing service: $($_.Exception.Message)\" -ForegroundColor Yellow\n        }\n    }\n    \n    & $nssmCommand install beszel-agent $AgentPath\n    if ($LASTEXITCODE -ne 0) {\n        throw \"Failed to install beszel-agent service\"\n    }\n    \n    Write-Host \"Configuring service environment variables...\"\n    & $nssmCommand set beszel-agent AppEnvironmentExtra \"+KEY=$Key\"\n    & $nssmCommand set beszel-agent AppEnvironmentExtra \"+TOKEN=$Token\"\n    & $nssmCommand set beszel-agent AppEnvironmentExtra \"+HUB_URL=$HubUrl\"\n    & $nssmCommand set beszel-agent AppEnvironmentExtra \"+PORT=$Port\"\n    \n    # Configure log files\n    $logDir = \"$env:ProgramData\\beszel-agent\\logs\"\n    if (-not (Test-Path $logDir)) {\n        New-Item -ItemType Directory -Path $logDir -Force | Out-Null\n    }\n    $logFile = \"$logDir\\beszel-agent.log\"\n    & $nssmCommand set beszel-agent AppStdout $logFile\n    & $nssmCommand set beszel-agent AppStderr $logFile\n}\n\n# Function to configure firewall rules\nfunction Configure-Firewall {\n    param (\n        [Parameter(Mandatory=$true)]\n        [int]$Port\n    )\n    \n    # Create a firewall rule if it doesn't exist\n    $ruleName = \"Allow beszel-agent\"\n    $existingRule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue\n    \n    # Remove existing rule if found\n    if ($existingRule) {\n        Write-Host \"Removing existing firewall rule...\"\n        try {\n            Remove-NetFirewallRule -DisplayName $ruleName\n            Write-Host \"Existing firewall rule removed successfully.\"\n        } catch {\n            Write-Host \"Warning: Failed to remove existing firewall rule: $($_.Exception.Message)\" -ForegroundColor Yellow\n        }\n    }\n    \n    # Create new rule with current settings\n    Write-Host \"Creating firewall rule for beszel-agent on port $Port...\"\n    try {\n        New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -LocalPort $Port\n        Write-Host \"Firewall rule created successfully.\"\n    } catch {\n        Write-Host \"Warning: Failed to create firewall rule: $($_.Exception.Message)\" -ForegroundColor Yellow\n        Write-Host \"You may need to manually create a firewall rule for port $Port.\" -ForegroundColor Yellow\n    }\n}\n\n# Function to start and monitor the service\nfunction Start-BeszelAgentService {\n    param (\n        [string]$NSSMPath = \"\"\n    )\n    \n    Write-Host \"Starting beszel-agent service...\"\n    \n    # Determine the NSSM executable to use\n    $nssmCommand = \"nssm\"\n    if ($NSSMPath -and (Test-Path $NSSMPath)) {\n        $nssmCommand = $NSSMPath\n    } elseif (-not (Test-CommandExists \"nssm\")) {\n        throw \"NSSM is not available in PATH and no valid NSSMPath was provided\"\n    }\n    \n    & $nssmCommand start beszel-agent\n    $startResult = $LASTEXITCODE\n    \n    # Only enter the status check loop if the NSSM start command failed\n    if ($startResult -ne 0) {\n        Write-Host \"NSSM start command returned error code: $startResult\" -ForegroundColor Yellow\n        Write-Host \"This could be due to 'SERVICE_START_PENDING' state. Checking service status...\"\n        \n        # Allow up to 10 seconds for the service to start, checking every second\n        $maxWaitTime = 10 # seconds\n        $elapsedTime = 0\n        $serviceStarted = $false\n        \n        while (-not $serviceStarted -and $elapsedTime -lt $maxWaitTime) {\n            Start-Sleep -Seconds 1\n            $elapsedTime += 1\n\n            $serviceStatus = & $nssmCommand status beszel-agent\n            \n            if ($serviceStatus -eq \"SERVICE_RUNNING\") {\n                $serviceStarted = $true\n                Write-Host \"Success! The beszel-agent service is now running.\" -ForegroundColor Green\n            }\n            elseif ($serviceStatus -like \"*PENDING*\") {\n                Write-Host \"Service is still starting (status: $serviceStatus)... waiting\" -ForegroundColor Yellow\n            }\n            else {\n                Write-Host \"Warning: The service status is '$serviceStatus' instead of 'SERVICE_RUNNING'.\" -ForegroundColor Yellow\n                Write-Host \"You may need to troubleshoot the service installation.\" -ForegroundColor Yellow\n                break\n            }\n        }\n        \n        if (-not $serviceStarted) {\n            Write-Host \"Service did not reach running state.\" -ForegroundColor Yellow\n            Write-Host \"You can check status manually with 'nssm status beszel-agent'\" -ForegroundColor Yellow\n        }\n    } else {\n        # NSSM start command was successful\n        Write-Host \"Success! The beszel-agent service is running properly.\" -ForegroundColor Green\n    }\n}\n\n#endregion\n\n#region Main Script Execution\n\n# Check if we're running as admin\n$isAdmin = Test-Admin\n\ntry {\n    # First: Install the agent (doesn't require admin)\n    if (-not $AgentPath) {\n        # Check for problematic case: running as admin and need Scoop\n        if ($isAdmin -and -not (Test-CommandExists \"scoop\") -and -not (Test-CommandExists \"winget\")) {\n            Write-Host \"ERROR: You're running as administrator but neither Scoop nor WinGet is available.\" -ForegroundColor Red\n            Write-Host \"Scoop should be installed without admin privileges.\" -ForegroundColor Red\n            Write-Host \"\" \n            Write-Host \"Please either:\" -ForegroundColor Yellow\n            Write-Host \"1. Run this script again without administrator privileges\" -ForegroundColor Yellow\n            Write-Host \"2. Install WinGet and run this script again\" -ForegroundColor Yellow\n            exit 1\n        }\n\n        if ($InstallMethod -eq \"Scoop\") {\n            if (-not (Test-CommandExists \"scoop\")) {\n                throw \"InstallMethod is set to Scoop, but Scoop is not available in PATH.\"\n            }\n            Write-Host \"Using Scoop for installation...\"\n            $AgentPath = Install-WithScoop -Key $Key -Port $Port\n        }\n        elseif ($InstallMethod -eq \"WinGet\") {\n            if (-not (Test-CommandExists \"winget\")) {\n                throw \"InstallMethod is set to WinGet, but WinGet is not available in PATH.\"\n            }\n            Write-Host \"Using WinGet for installation...\"\n            $AgentPath = Install-WithWinGet -Key $Key -Port $Port\n        }\n        else {\n            if (Test-CommandExists \"scoop\") {\n                Write-Host \"Using Scoop for installation...\"\n                $AgentPath = Install-WithScoop -Key $Key -Port $Port\n            }\n            elseif (Test-CommandExists \"winget\") {\n                Write-Host \"Using WinGet for installation...\"\n                $AgentPath = Install-WithWinGet -Key $Key -Port $Port\n            }\n            else {\n                Write-Host \"Neither Scoop nor WinGet is installed. Installing Scoop...\"\n                $AgentPath = Install-WithScoop -Key $Key -Port $Port\n            }\n        }\n    }\n\n    if (-not $AgentPath) {\n        throw \"Could not find beszel-agent executable. Make sure it was properly installed.\"\n    }\n    \n    # Find NSSM path if not already provided\n    if (-not $NSSMPath) {\n        $NSSMPath = Find-NSSM\n        \n        if (-not $NSSMPath -and (Test-CommandExists \"nssm\")) {\n            $NSSMPath = (Get-Command \"nssm\" -ErrorAction SilentlyContinue).Source\n        }\n        \n        # If we still don't have NSSM, try to install it if we have package managers\n        if (-not $NSSMPath) {\n            if (Test-CommandExists \"winget\") {\n                Write-Host \"NSSM not found. Attempting to install via WinGet...\"\n                try {\n                    Install-NSSM -Method \"WinGet\"\n                    $NSSMPath = Find-NSSM\n                    if (-not $NSSMPath -and (Test-CommandExists \"nssm\")) {\n                        $NSSMPath = (Get-Command \"nssm\" -ErrorAction SilentlyContinue).Source\n                    }\n                } catch {\n                    Write-Host \"Failed to install NSSM via WinGet: $($_.Exception.Message)\" -ForegroundColor Yellow\n                }\n            } elseif (Test-CommandExists \"scoop\") {\n                Write-Host \"NSSM not found. Attempting to install via Scoop...\"\n                try {\n                    Install-NSSM -Method \"Scoop\"\n                    $NSSMPath = Find-NSSM\n                    if (-not $NSSMPath -and (Test-CommandExists \"nssm\")) {\n                        $NSSMPath = (Get-Command \"nssm\" -ErrorAction SilentlyContinue).Source\n                    }\n                } catch {\n                    Write-Host \"Failed to install NSSM via Scoop: $($_.Exception.Message)\" -ForegroundColor Yellow\n                }\n            }\n            \n            # Final check - if we still don't have NSSM and we're admin, we have a problem\n            if (-not $NSSMPath -and ($isAdmin -or $Elevated)) {\n                throw \"NSSM is required for service installation but was not found and could not be installed. Please install NSSM manually or run as a regular user to install it.\"\n            }\n        }\n    }\n    \n    # Second: If we need admin rights for service installation and we don't have them, relaunch\n    if (-not $isAdmin -and -not $Elevated) {\n        Write-Host \"Admin privileges required for service installation. Relaunching as admin...\" -ForegroundColor Yellow\n        Write-Host \"Check service status with 'nssm status beszel-agent'\"\n        Write-Host \"Edit service configuration with 'nssm edit beszel-agent'\"\n        \n        # Prepare arguments for the elevated script\n        $argumentList = @(\n            \"-ExecutionPolicy\", \"Bypass\",\n            \"-File\", \"`\"$PSCommandPath`\"\",\n            \"-Elevated\",\n            \"-Key\", \"`\"$Key`\"\",\n            \"-Token\", \"`\"$Token`\"\",\n            \"-Url\", \"`\"$Url`\"\",\n            \"-Port\", $Port,\n            \"-AgentPath\", \"`\"$AgentPath`\"\",\n            \"-InstallMethod\", $InstallMethod\n        )\n        \n        # Add NSSMPath if we found it\n        if ($NSSMPath) {\n            $argumentList += \"-NSSMPath\"\n            $argumentList += \"`\"$NSSMPath`\"\"\n        }\n\n        if ($ConfigureFirewall) {\n            $argumentList += \"-ConfigureFirewall\"\n        }\n        \n        # Relaunch the script with the -Elevated switch and pass parameters\n        Start-Process powershell.exe -Verb RunAs -ArgumentList $argumentList\n        exit\n    }\n    \n    # Third: If we have admin rights, install service and configure firewall\n    if ($isAdmin -or $Elevated) {\n        # Install the service\n        Install-NSSMService -AgentPath $AgentPath -Key $Key -Token $Token -HubUrl $Url -Port $Port -NSSMPath $NSSMPath\n        \n        if ($ConfigureFirewall) {\n            Configure-Firewall -Port $Port\n        } else {\n            Write-Host \"Skipping firewall configuration. Use -ConfigureFirewall to add an inbound rule for port $Port.\" -ForegroundColor Yellow\n        }\n        \n        # Start the service\n        Start-BeszelAgentService -NSSMPath $NSSMPath\n        \n        # Pause to see results if this is an elevated window\n        if ($Elevated) {\n            Write-Host \"Press any key to exit...\" -ForegroundColor Cyan\n            $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n        }\n    }\n}\ncatch {\n    Write-Host \"ERROR: $($_.Exception.Message)\" -ForegroundColor Red\n    Write-Host \"Installation failed. Please check the error message above.\" -ForegroundColor Red\n    \n    # Pause if this is likely a new window\n    if ($Elevated -or (-not $isAdmin)) {\n        Write-Host \"Press any key to exit...\" -ForegroundColor Red\n        $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n    }\n    exit 1\n}\n\n#endregion\n"
  },
  {
    "path": "supplemental/scripts/install-agent.sh",
    "content": "#!/bin/sh\n\nis_alpine() {\n  [ -f /etc/alpine-release ]\n}\n\nis_openwrt() {\n  grep -qi \"OpenWrt\" /etc/os-release\n}\n\nis_freebsd() {\n  [ \"$(uname -s)\" = \"FreeBSD\" ]\n}\n\nis_glibc() {\n  # Prefer glibc-enabled agent (NVML via purego) on linux/amd64 glibc systems.\n  # Check common dynamic loader paths first (fast + reliable).\n  for p in \\\n    /lib64/ld-linux-x86-64.so.2 \\\n    /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \\\n    /lib/ld-linux-x86-64.so.2; do\n    [ -e \"$p\" ] && return 0\n  done\n\n  # Fallback to ldd output if available.\n  if command -v ldd >/dev/null 2>&1; then\n    ldd --version 2>&1 | grep -qiE 'gnu libc|glibc' && return 0\n  fi\n\n  return 1\n}\n\n\n# If SELinux is enabled, set the context of the binary\nset_selinux_context() {\n  # Check if SELinux is enabled and in enforcing or permissive mode\n  if command -v getenforce >/dev/null 2>&1; then\n    SELINUX_MODE=$(getenforce)\n    if [ \"$SELINUX_MODE\" != \"Disabled\" ]; then\n      echo \"SELinux is enabled (${SELINUX_MODE} mode). Setting appropriate context...\"\n\n      # First try to set persistent context if semanage is available\n      if command -v semanage >/dev/null 2>&1; then\n        echo \"Attempting to set persistent SELinux context...\"\n        if semanage fcontext -a -t bin_t \"$BIN_PATH\" >/dev/null 2>&1; then\n          restorecon -v \"$BIN_PATH\" >/dev/null 2>&1\n        else\n          echo \"Warning: Failed to set persistent context, falling back to temporary context.\"\n        fi\n      fi\n\n      # Fall back to chcon if semanage failed or isn't available\n      if command -v chcon >/dev/null 2>&1; then\n        # Set context for both the directory and binary\n        chcon -t bin_t \"$BIN_PATH\" || echo \"Warning: Failed to set SELinux context for binary.\"\n        chcon -R -t bin_t \"$AGENT_DIR\" || echo \"Warning: Failed to set SELinux context for directory.\"\n      else\n        if [ \"$SELINUX_MODE\" = \"Enforcing\" ]; then\n          echo \"Warning: SELinux is in enforcing mode but chcon command not found. The service may fail to start.\"\n          echo \"Consider installing the policycoreutils package or temporarily setting SELinux to permissive mode.\"\n        else\n          echo \"Warning: SELinux is in permissive mode but chcon command not found.\"\n        fi\n      fi\n    fi\n  fi\n}\n\n# Clean up SELinux contexts if they were set\ncleanup_selinux_context() {\n  if command -v getenforce >/dev/null 2>&1 && [ \"$(getenforce)\" != \"Disabled\" ]; then\n    echo \"Cleaning up SELinux contexts...\"\n    # Remove persistent context if semanage is available\n    if command -v semanage >/dev/null 2>&1; then\n      semanage fcontext -d \"$BIN_PATH\" 2>/dev/null || true\n    fi\n  fi\n}\n\n# Ensure the proxy URL ends with a /\nensure_trailing_slash() {\n  if [ -n \"$1\" ]; then\n    case \"$1\" in\n    */) echo \"$1\" ;;\n    *) echo \"$1/\" ;;\n    esac\n  else\n    echo \"$1\"\n  fi\n}\n\n# Generate FreeBSD rc service content\ngenerate_freebsd_rc_service() {\n  cat <<'EOF'\n#!/bin/sh\n\n# PROVIDE: beszel_agent\n# REQUIRE: DAEMON NETWORKING\n# BEFORE: LOGIN\n# KEYWORD: shutdown\n\n# Add the following lines to /etc/rc.conf to configure Beszel Agent:\n#\n# beszel_agent_enable (bool):   Set to YES to enable Beszel Agent\n#                               Default: YES\n# beszel_agent_env_file (str):  Beszel Agent env configuration file\n#                               Default: /usr/local/etc/beszel-agent/env\n# beszel_agent_user (str):      Beszel Agent daemon user\n#                               Default: beszel\n# beszel_agent_bin (str):       Path to the beszel-agent binary\n#                               Default: /usr/local/sbin/beszel-agent\n# beszel_agent_flags (str):     Extra flags passed to beszel-agent command invocation\n#                               Default:\n\n. /etc/rc.subr\n\nname=\"beszel_agent\"\nrcvar=beszel_agent_enable\n\nload_rc_config $name\n: ${beszel_agent_enable:=\"YES\"}\n: ${beszel_agent_user:=\"beszel\"}\n: ${beszel_agent_flags:=\"\"}\n: ${beszel_agent_env_file:=\"/usr/local/etc/beszel-agent/env\"}\n: ${beszel_agent_bin:=\"/usr/local/sbin/beszel-agent\"}\n\nlogfile=\"/var/log/${name}.log\"\npidfile=\"/var/run/${name}.pid\"\n\nprocname=\"/usr/sbin/daemon\"\nstart_precmd=\"${name}_prestart\"\nstart_cmd=\"${name}_start\"\nstop_cmd=\"${name}_stop\"\n\nextra_commands=\"upgrade\"\nupgrade_cmd=\"beszel_agent_upgrade\"\n\nbeszel_agent_prestart()\n{\n    if [ ! -f \"${beszel_agent_env_file}\" ]; then\n        echo WARNING: missing \"${beszel_agent_env_file}\" env file. Start aborted.\n        exit 1\n    fi\n}\n\nbeszel_agent_start()\n{\n    echo \"Starting ${name}\"\n    /usr/sbin/daemon -fc \\\n            -P \"${pidfile}\" \\\n            -o \"${logfile}\" \\\n            -u \"${beszel_agent_user}\" \\\n            \"${beszel_agent_bin}\" ${beszel_agent_flags}\n}\n\nbeszel_agent_stop()\n{\n    pid=\"$(check_pidfile \"${pidfile}\" \"${procname}\")\"\n    if [ -n \"${pid}\" ]; then\n        echo \"Stopping ${name} (pid=${pid})\"\n        kill -- \"-${pid}\"\n        wait_for_pids \"${pid}\"\n    else\n        echo \"${name} isn't running\"\n    fi\n}\n\nbeszel_agent_upgrade()\n{\n    echo \"Upgrading ${name}\"\n    if command -v sudo >/dev/null; then\n        sudo -u \"${beszel_agent_user}\" -- \"${beszel_agent_bin}\" update\n    else\n        su -m \"${beszel_agent_user}\" -c \"${beszel_agent_bin} update\"\n    fi\n}\n\nrun_rc_command \"$1\"\nEOF\n}\n\n# Detect system architecture\ndetect_architecture() {\n  local arch=$(uname -m)\n\n  if [ \"$arch\" = \"mips\" ]; then\n    detect_mips_endianness\n    return $?\n  fi\n\n  case \"$arch\" in\n    x86_64)\n      arch=\"amd64\"\n      ;;\n    armv6l|armv7l)\n      arch=\"arm\"\n      ;;\n    aarch64)\n      arch=\"arm64\"\n      ;;\n  esac\n\n  echo \"$arch\"\n}\n\n# Detect MIPS endianness using ELF header\ndetect_mips_endianness() {\n  local bins=\"/bin/sh /bin/ls /usr/bin/env\"\n  local bin_to_check endian\n  \n  for bin_to_check in $bins; do\n    if [ -f \"$bin_to_check\" ]; then\n      # The 6th byte in ELF header: 01 = little, 02 = big\n      endian=$(hexdump -n 1 -s 5 -e '1/1 \"%02x\"' \"$bin_to_check\" 2>/dev/null)\n      if [ \"$endian\" = \"01\" ]; then\n        echo \"mipsle\"\n        return\n      elif [ \"$endian\" = \"02\" ]; then\n        echo \"mips\" \n        return\n      fi\n    fi\n  done\n  \n  # Final fallback\n  echo \"mips\"\n}\n\n# Default values\nPORT=45876\nUNINSTALL=false\nGITHUB_URL=\"https://github.com\"\nGITHUB_PROXY_URL=\"\"\nKEY=\"\"\nTOKEN=\"\"\nHUB_URL=\"\"\nAUTO_UPDATE_FLAG=\"\" # empty string means prompt, \"true\" means auto-enable, \"false\" means skip\nVERSION=\"latest\"\n\n# Check for help flag\ncase \"$1\" in\n-h | --help)\n  printf \"Beszel Agent installation script\\n\\n\"\n  printf \"Usage: ./install-agent.sh [options]\\n\\n\"\n  printf \"Options: \\n\"\n  printf \"  -k                    : SSH key (required, or interactive if not provided)\\n\"\n  printf \"  -p                    : Port (default: $PORT)\\n\"\n  printf \"  -t                    : Token (optional for backwards compatibility)\\n\"\n  printf \"  -url                  : Hub URL (optional for backwards compatibility)\\n\"\n  printf \"  -v, --version         : Version to install (default: latest)\\n\"\n  printf \"  -u                    : Uninstall Beszel Agent\\n\"\n  printf \"  --auto-update [VALUE] : Control automatic daily updates\\n\"\n  printf \"                          VALUE can be true (enable) or false (disable). If not specified, will prompt.\\n\"\n  printf \"  --mirror [URL]        : Use GitHub proxy to resolve network timeout issues in mainland China\\n\"\n  printf \"                          URL: optional custom proxy URL (default: https://gh.beszel.dev)\\n\"\n  printf \"  -h, --help            : Display this help message\\n\"\n  exit 0\n  ;;\nesac\n\n# Build sudo args by properly quoting everything\nbuild_sudo_args() {\n  QUOTED_ARGS=\"\"\n  while [ $# -gt 0 ]; do\n    if [ -n \"$QUOTED_ARGS\" ]; then\n      QUOTED_ARGS=\"$QUOTED_ARGS \"\n    fi\n    QUOTED_ARGS=\"$QUOTED_ARGS'$(echo \"$1\" | sed \"s/'/'\\\\\\\\''/g\")'\"\n    shift\n  done\n  echo \"$QUOTED_ARGS\"\n}\n\n# Check if running as root and re-execute with sudo if needed\nif [ \"$(id -u)\" != \"0\" ]; then\n  if command -v sudo >/dev/null 2>&1; then\n    SUDO_ARGS=$(build_sudo_args \"$@\")\n    eval \"exec sudo $0 $SUDO_ARGS\"\n  else\n    echo \"This script must be run as root. Please either:\"\n    echo \"1. Run this script as root (su root)\"\n    echo \"2. Install sudo and run with sudo\"\n    exit 1\n  fi\nfi\n\n# Parse arguments\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n  -k)\n    shift\n    KEY=\"$1\"\n    ;;\n  -p)\n    shift\n    PORT=\"$1\"\n    ;;\n  -t)\n    shift\n    TOKEN=\"$1\"\n    ;;\n  -url)\n    shift\n    HUB_URL=\"$1\"\n    ;;\n  -v | --version)\n    shift\n    VERSION=\"$1\"\n    ;;\n  -u)\n    UNINSTALL=true\n    ;;\n  --mirror* | --china-mirrors*)\n    # Check if there's a value after the = sign\n    if echo \"$1\" | grep -q \"=\"; then\n      # Extract the value after =\n      CUSTOM_PROXY=$(echo \"$1\" | cut -d'=' -f2)\n      if [ -n \"$CUSTOM_PROXY\" ]; then\n        GITHUB_PROXY_URL=\"$CUSTOM_PROXY\"\n        GITHUB_URL=\"$(ensure_trailing_slash \"$CUSTOM_PROXY\")https://github.com\"\n      else\n        GITHUB_PROXY_URL=\"https://gh.beszel.dev\"\n        GITHUB_URL=\"$GITHUB_PROXY_URL\"\n      fi\n    elif [ \"$2\" != \"\" ] && ! echo \"$2\" | grep -q '^-'; then\n      # use custom proxy URL provided as next argument\n      GITHUB_PROXY_URL=\"$2\"\n      GITHUB_URL=\"$(ensure_trailing_slash \"$2\")https://github.com\"\n      shift\n    else\n      # No value specified, use default\n      GITHUB_PROXY_URL=\"https://gh.beszel.dev\"\n      GITHUB_URL=\"$GITHUB_PROXY_URL\"\n    fi\n    ;;\n  --auto-update*)\n    # Check if there's a value after the = sign\n    if echo \"$1\" | grep -q \"=\"; then\n      # Extract the value after =\n      AUTO_UPDATE_VALUE=$(echo \"$1\" | cut -d'=' -f2)\n      if [ \"$AUTO_UPDATE_VALUE\" = \"true\" ]; then\n        AUTO_UPDATE_FLAG=\"true\"\n      elif [ \"$AUTO_UPDATE_VALUE\" = \"false\" ]; then\n        AUTO_UPDATE_FLAG=\"false\"\n      else\n        echo \"Invalid value for --auto-update flag: $AUTO_UPDATE_VALUE. Using default (prompt).\"\n      fi\n    elif [ \"$2\" = \"true\" ] || [ \"$2\" = \"false\" ]; then\n      # Value provided as next argument\n      AUTO_UPDATE_FLAG=\"$2\"\n      shift\n    else\n      # No value specified, use true\n      AUTO_UPDATE_FLAG=\"true\"\n    fi\n    ;;\n  *)\n    echo \"Invalid option: $1\" >&2\n    exit 1\n    ;;\n  esac\n  shift\ndone\n\n# Set paths based on operating system\nif is_freebsd; then\n  AGENT_DIR=\"/usr/local/etc/beszel-agent\"\n  BIN_DIR=\"/usr/local/sbin\"\n  BIN_PATH=\"/usr/local/sbin/beszel-agent\"\nelse\n  AGENT_DIR=\"/opt/beszel-agent\"\n  BIN_DIR=\"/opt/beszel-agent\"\n  BIN_PATH=\"/opt/beszel-agent/beszel-agent\"\nfi\n\n# Stop existing service if it exists (for upgrades)\nif [ \"$UNINSTALL\" != true ] && [ -f \"$BIN_PATH\" ]; then\n  echo \"Existing installation detected. Stopping service for upgrade...\"\n  if is_alpine; then\n    rc-service beszel-agent stop 2>/dev/null || true\n  elif is_openwrt; then\n    /etc/init.d/beszel-agent stop 2>/dev/null || true\n  elif is_freebsd; then\n    service beszel-agent stop 2>/dev/null || true\n  else\n    systemctl stop beszel-agent.service 2>/dev/null || true\n  fi\nfi\n\n# Uninstall process\nif [ \"$UNINSTALL\" = true ]; then\n  # Clean up SELinux contexts before removing files\n  cleanup_selinux_context\n\n  if is_alpine; then\n    echo \"Stopping and disabling the agent service...\"\n    rc-service beszel-agent stop\n    rc-update del beszel-agent default\n\n    echo \"Removing the OpenRC service files...\"\n    rm -f /etc/init.d/beszel-agent\n\n    # Remove the daily update cron job if it exists\n    echo \"Removing the daily update cron job...\"\n    if crontab -u root -l 2>/dev/null | grep -q \"beszel-agent.*update\"; then\n      crontab -u root -l 2>/dev/null | grep -v \"beszel-agent.*update\" | crontab -u root -\n    fi\n\n    # Remove log files\n    echo \"Removing log files...\"\n    rm -f /var/log/beszel-agent.log /var/log/beszel-agent.err\n  elif is_openwrt; then\n    echo \"Stopping and disabling the agent service...\"\n    /etc/init.d/beszel-agent stop\n    /etc/init.d/beszel-agent disable\n\n    echo \"Removing the OpenWRT service files...\"\n    rm -f /etc/init.d/beszel-agent\n\n    # Remove the update service if it exists\n    echo \"Removing the daily update service...\"\n    # Remove legacy beszel account based crontab file\n    rm -f /etc/crontabs/beszel\n    # Install root crontab job\n    if crontab -u root -l 2>/dev/null | grep -q \"beszel-agent.*update\"; then\n      crontab -u root -l 2>/dev/null | grep -v \"beszel-agent.*update\" | crontab -u root -\n    fi\n\n  elif is_freebsd; then\n    echo \"Stopping and disabling the agent service...\"\n    service beszel-agent stop\n    sysrc beszel_agent_enable=\"NO\"\n\n    echo \"Removing the FreeBSD service files...\"\n    rm -f /usr/local/etc/rc.d/beszel-agent\n\n    # Remove the daily update cron job if it exists\n    echo \"Removing the daily update cron job...\"\n    rm -f /etc/cron.d/beszel-agent\n\n    # Remove log files\n    echo \"Removing log files...\"\n    rm -f /var/log/beszel-agent.log\n\n    # Remove env file and directories\n    echo \"Removing environment configuration file...\"\n    rm -f \"$AGENT_DIR/env\"\n    rm -f \"$BIN_PATH\"\n    rmdir \"$AGENT_DIR\" 2>/dev/null || true\n\n  else\n    echo \"Stopping and disabling the agent service...\"\n    systemctl stop beszel-agent.service\n    systemctl disable beszel-agent.service >/dev/null 2>&1\n\n    echo \"Removing the systemd service file...\"\n    rm /etc/systemd/system/beszel-agent.service\n\n    # Remove the update timer and service if they exist\n    echo \"Removing the daily update service and timer...\"\n    systemctl stop beszel-agent-update.timer 2>/dev/null\n    systemctl disable beszel-agent-update.timer >/dev/null 2>&1\n    rm -f /etc/systemd/system/beszel-agent-update.service\n    rm -f /etc/systemd/system/beszel-agent-update.timer\n\n    systemctl daemon-reload\n  fi\n\n  echo \"Removing the Beszel Agent directory...\"\n  rm -rf \"$AGENT_DIR\"\n\n  echo \"Removing the dedicated user for the agent service...\"\n  killall beszel-agent 2>/dev/null\n  if is_alpine || is_openwrt; then\n    deluser beszel 2>/dev/null\n  elif is_freebsd; then\n    pw user del beszel 2>/dev/null\n  else\n    userdel beszel 2>/dev/null\n  fi\n\n  echo \"Beszel Agent has been uninstalled successfully!\"\n  exit 0\nfi\n\n# Check if a package is installed\npackage_installed() {\n  command -v \"$1\" >/dev/null 2>&1\n}\n\n# Check for package manager and install necessary packages if not installed\nif package_installed apk; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    apk update\n    apk add tar curl coreutils shadow\n  fi\nelif package_installed opkg; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    opkg update\n    opkg install tar curl coreutils\n  fi\nelif package_installed pkg && is_freebsd; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    pkg update\n    pkg install -y gtar curl coreutils\n  fi\nelif package_installed apt-get; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    apt-get update\n    apt-get install -y tar curl coreutils\n  fi\nelif package_installed yum; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    yum install -y tar curl coreutils\n  fi\nelif package_installed pacman; then\n  if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then\n    pacman -Sy --noconfirm tar curl coreutils\n  fi\nelse\n  echo \"Warning: Please ensure 'tar' and 'curl' and 'sha256sum (coreutils)' are installed.\"\nfi\n\n# If no SSH key is provided, ask for the SSH key interactively (skip if upgrading)\nif [ -z \"$KEY\" ]; then\n  if [ -f \"$BIN_PATH\" ]; then\n    echo \"Upgrading existing installation. Using existing service configuration.\"\n  else\n    printf \"Enter your SSH key: \"\n    read KEY\n  fi\nfi\n\n# Remove newlines from KEY\nKEY=$(echo \"$KEY\" | tr -d '\\n')\n\n# TOKEN and HUB_URL are optional for backwards compatibility - no interactive prompts\n# They will be set as empty environment variables if not provided\n\n# Verify checksum\nif command -v sha256sum >/dev/null; then\n  CHECK_CMD=\"sha256sum\"\nelif command -v sha256 >/dev/null; then\n  # FreeBSD uses 'sha256' instead of 'sha256sum', with different output format\n  CHECK_CMD=\"sha256 -q\"\nelse\n  echo \"No SHA256 checksum utility found\"\n  exit 1\nfi\n\n# Create a dedicated user for the service if it doesn't exist\necho \"Configuring the dedicated user for the Beszel Agent service...\"\nif is_alpine; then\n  if ! id -u beszel >/dev/null 2>&1; then\n    addgroup beszel\n    adduser -S -D -H -s /sbin/nologin -G beszel beszel\n  fi\n  # Add the user to the docker group to allow access to the Docker socket if group docker exists\n  if getent group docker >/dev/null 2>&1; then\n    echo \"Adding beszel to docker group\"\n    addgroup beszel docker\n  fi\n  \nelif is_openwrt; then\n  # Create beszel group first if it doesn't exist (check /etc/group directly)\n  if ! grep -q \"^beszel:\" /etc/group >/dev/null 2>&1; then\n    echo \"beszel:x:999:\" >> /etc/group\n  fi\n  \n  # Create beszel user if it doesn't exist (double-check to prevent duplicates)\n  if ! id -u beszel >/dev/null 2>&1 && ! grep -q \"^beszel:\" /etc/passwd >/dev/null 2>&1; then\n    echo \"beszel:x:999:999::/nonexistent:/bin/false\" >> /etc/passwd\n  fi\n  \n  # Add the user to the docker group if docker group exists and user is not already in it\n  if grep -q \"^docker:\" /etc/group >/dev/null 2>&1; then\n    echo \"Adding beszel to docker group\"\n    # Check if beszel is already in docker group\n    if ! grep \"^docker:\" /etc/group | grep -q \"beszel\"; then\n      # Add beszel to docker group by modifying /etc/group\n      # Handle both cases: group with existing members and group without members\n      if grep \"^docker:\" /etc/group | grep -q \":.*:.*$\"; then\n        # Group has existing members, append with comma\n        sed -i 's/^docker:\\([^:]*:[^:]*:\\)\\(.*\\)$/docker:\\1\\2,beszel/' /etc/group\n      else\n        # Group has no members, just append\n        sed -i 's/^docker:\\([^:]*:[^:]*:\\)$/docker:\\1beszel/' /etc/group\n      fi\n    fi\n  fi\n\nelif is_freebsd; then\n  if ! id -u beszel >/dev/null 2>&1; then\n    pw user add beszel -d /nonexistent -s /usr/sbin/nologin -c \"beszel user\"\n  fi\n  # Add the user to the wheel group to allow self-updates\n  if pw group show wheel >/dev/null 2>&1; then\n    echo \"Adding beszel to wheel group for self-updates\"\n    pw group mod wheel -m beszel\n  fi\n\nelse\n  if ! id -u beszel >/dev/null 2>&1; then\n    useradd --system --home-dir /nonexistent --shell /bin/false beszel\n  fi\n  # Add the user to the docker group to allow access to the Docker socket if group docker exists\n  if getent group docker >/dev/null 2>&1; then\n    echo \"Adding beszel to docker group\"\n    usermod -aG docker beszel\n  fi\n  # Add the user to the disk group to allow access to disk devices if group disk exists\n  if getent group disk >/dev/null 2>&1; then\n    echo \"Adding beszel to disk group\"\n    usermod -aG disk beszel\n  fi\nfi\n\n# Create the directory for the Beszel Agent\n\nif [ ! -d \"$AGENT_DIR\" ]; then\n  echo \"Creating the directory for the Beszel Agent...\"\n  mkdir -p \"$AGENT_DIR\"\n  chown beszel:beszel \"$AGENT_DIR\"\n  chmod 755 \"$AGENT_DIR\"\nfi\n\nif [ ! -d \"$BIN_DIR\" ]; then\n  mkdir -p \"$BIN_DIR\"\nfi\n\n# Download and install the Beszel Agent\n\nOS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')\nARCH=$(detect_architecture)\nFILE_NAME=\"beszel-agent_${OS}_${ARCH}.tar.gz\"\nif [ \"$OS\" = \"linux\" ] && [ \"$ARCH\" = \"amd64\" ] && is_glibc; then\n  FILE_NAME=\"beszel-agent_${OS}_${ARCH}_glibc.tar.gz\"\nfi\n\n# Determine version to install\nif [ \"$VERSION\" = \"latest\" ]; then\n  INSTALL_VERSION=$(curl -s \"https://get.beszel.dev/latest-version\")\n  if [ -z \"$INSTALL_VERSION\" ]; then\n    # Fallback to GitHub API\n    API_RELEASE_URL=\"https://api.github.com/repos/henrygd/beszel/releases/latest\"\n    INSTALL_VERSION=$(curl -s \"$API_RELEASE_URL\" | grep -o '\"tag_name\": \"v[^\"]*\"' | cut -d'\"' -f4 | tr -d 'v')\n  fi\n  if [ -z \"$INSTALL_VERSION\" ]; then\n    echo \"Failed to get latest version\"\n    exit 1\n  fi\nelse\n  INSTALL_VERSION=\"$VERSION\"\n  # Remove 'v' prefix if present\n  INSTALL_VERSION=$(echo \"$INSTALL_VERSION\" | sed 's/^v//')\nfi\n\necho \"Downloading beszel-agent v${INSTALL_VERSION}...\"\n\n# Download checksums file\nTEMP_DIR=$(mktemp -d)\ncd \"$TEMP_DIR\" || exit 1\nCHECKSUM=$(curl -fsSL \"$GITHUB_URL/henrygd/beszel/releases/download/v${INSTALL_VERSION}/beszel_${INSTALL_VERSION}_checksums.txt\" | grep \"$FILE_NAME\" | cut -d' ' -f1)\nif [ -z \"$CHECKSUM\" ] || ! echo \"$CHECKSUM\" | grep -qE \"^[a-fA-F0-9]{64}$\"; then\n  echo \"Failed to get checksum or invalid checksum format\"\n  echo \"Try again with --mirror (or --mirror <url>) if GitHub is not reachable.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif ! curl -fL# --retry 3 --retry-delay 2 --connect-timeout 10 \"$GITHUB_URL/henrygd/beszel/releases/download/v${INSTALL_VERSION}/$FILE_NAME\" -o \"$FILE_NAME\"; then\n  echo \"Failed to download the agent from $GITHUB_URL/henrygd/beszel/releases/download/v${INSTALL_VERSION}/$FILE_NAME\"\n  echo \"Try again with --mirror (or --mirror <url>) if GitHub is not reachable.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif ! tar -tzf \"$FILE_NAME\" >/dev/null 2>&1; then\n  echo \"Downloaded archive is invalid or incomplete (possible network/proxy issue).\"\n  echo \"Try again with --mirror (or --mirror <url>) if the download path is unstable.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif [ \"$($CHECK_CMD \"$FILE_NAME\" | cut -d' ' -f1)\" != \"$CHECKSUM\" ]; then\n  echo \"Checksum verification failed: $($CHECK_CMD \"$FILE_NAME\" | cut -d' ' -f1) & $CHECKSUM\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif ! tar -xzf \"$FILE_NAME\" beszel-agent; then\n  echo \"Failed to extract the agent\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif [ ! -s \"$TEMP_DIR/beszel-agent\" ]; then\n  echo \"Downloaded binary is missing or empty.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif [ -f \"$BIN_PATH\" ]; then\n  echo \"Backing up existing binary...\"\n  cp \"$BIN_PATH\" \"$BIN_PATH.bak\"\nfi\n\nmv beszel-agent \"$BIN_PATH\"\nchown beszel:beszel \"$BIN_PATH\"\nchmod 755 \"$BIN_PATH\"\n\n# Set SELinux context if needed\nset_selinux_context\n\n# Cleanup\nrm -rf \"$TEMP_DIR\"\n\n# Make sure /etc/machine-id exists for persistent fingerprint\nif [ ! -f /etc/machine-id ]; then\n  cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id\nfi\n\n# Check for NVIDIA GPUs and grant device permissions for systemd service\ndetect_nvidia_devices() {\n  local devices=\"\"\n  for i in /dev/nvidia*; do\n    if [ -e \"$i\" ]; then\n      devices=\"${devices}DeviceAllow=$i rw\\n\"\n    fi\n  done\n  echo \"$devices\"\n}\n\n# Modify service installation part, add Alpine check before systemd service creation\nif is_alpine; then\n  if [ ! -f /etc/init.d/beszel-agent ]; then\n    echo \"Creating OpenRC service for Alpine Linux...\"\n    cat >/etc/init.d/beszel-agent <<EOF\n#!/sbin/openrc-run\n\nname=\"beszel-agent\"\ndescription=\"Beszel Agent Service\"\ncommand=\"$BIN_PATH\"\ncommand_user=\"beszel\"\ncommand_background=\"yes\"\npidfile=\"/run/\\${RC_SVCNAME}.pid\"\noutput_log=\"/var/log/beszel-agent.log\"\nerror_log=\"/var/log/beszel-agent.err\"\n\nstart_pre() {\n    checkpath -f -m 0644 -o beszel:beszel \"\\$output_log\" \"\\$error_log\"\n}\n\nexport PORT=\"$PORT\"\nexport KEY=\"$KEY\"\nexport TOKEN=\"$TOKEN\"\nexport HUB_URL=\"$HUB_URL\"\n\ndepend() {\n    need net\n    after firewall\n}\nEOF\n    chmod +x /etc/init.d/beszel-agent\n    rc-update add beszel-agent default\n  else\n    echo \"Alpine OpenRC service file already exists. Skipping creation.\"\n  fi\n\n  # Create log files with proper permissions\n  touch /var/log/beszel-agent.log /var/log/beszel-agent.err\n  chown beszel:beszel /var/log/beszel-agent.log /var/log/beszel-agent.err\n\n  # Start the service\n  rc-service beszel-agent restart\n\n  # Check if service started successfully\n  sleep 2\n  if ! rc-service beszel-agent status | grep -q \"started\"; then\n    echo \"Error: The Beszel Agent service failed to start. Checking logs...\"\n    tail -n 20 /var/log/beszel-agent.err\n    exit 1\n  fi\n\n  # Auto-update service for Alpine\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    AUTO_UPDATE=\"y\"\n  elif [ \"$AUTO_UPDATE_FLAG\" = \"false\" ]; then\n    AUTO_UPDATE=\"n\"\n  else\n    printf \"\\nEnable automatic daily updates for beszel-agent? (y/n): \"\n    read AUTO_UPDATE\n  fi\n  case \"$AUTO_UPDATE\" in\n  [Yy]*)\n    echo \"Setting up daily automatic updates for beszel-agent...\"\n\n    # Create cron job to run beszel-agent update command daily at midnight\n    if ! crontab -u root -l 2>/dev/null | grep -q \"beszel-agent.*update\"; then\n      (crontab -u root -l 2>/dev/null; echo \"12 0 * * * $BIN_PATH update >/dev/null 2>&1\") | crontab -u root -\n    fi\n\n    printf \"\\nDaily updates have been enabled via cron job.\\n\"\n    ;;\n  esac\n\n  # Check service status\n  if ! rc-service beszel-agent status >/dev/null 2>&1; then\n    echo \"Error: The Beszel Agent service is not running.\"\n    rc-service beszel-agent status\n    exit 1\n  fi\n\nelif is_openwrt; then\n  if [ ! -f /etc/init.d/beszel-agent ]; then\n    echo \"Creating procd init script service for OpenWRT...\"\n    cat >/etc/init.d/beszel-agent <<EOF\n#!/bin/sh /etc/rc.common\n\nUSE_PROCD=1\nSTART=99\n\nstart_service() {\n    procd_open_instance\n    procd_set_param command $BIN_PATH\n    procd_set_param user beszel\n    procd_set_param pidfile /var/run/beszel-agent.pid\n    procd_set_param env PORT=\"$PORT\" KEY=\"$KEY\" TOKEN=\"$TOKEN\" HUB_URL=\"$HUB_URL\"\n    procd_set_param respawn\n    procd_set_param stdout 1\n    procd_set_param stderr 1\n    procd_close_instance\n}\n\n# Extra command to trigger agent update\nEXTRA_COMMANDS=\"update restart\"\nEXTRA_HELP=\"        update          Update the Beszel agent\n        restart         Restart the Beszel agent\"\n\nupdate() {\n    $BIN_PATH update\n}\n\nEOF\n    # Enable the service\n    chmod +x /etc/init.d/beszel-agent\n    /etc/init.d/beszel-agent enable\n  else\n    echo \"OpenWRT init script already exists. Skipping creation.\"\n  fi\n\n  # Start the service\n  /etc/init.d/beszel-agent restart\n\n  # Auto-update service for OpenWRT using a crontab job\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    AUTO_UPDATE=\"y\"\n    sleep 1 # give time for the service to start\n  elif [ \"$AUTO_UPDATE_FLAG\" = \"false\" ]; then\n    AUTO_UPDATE=\"n\"\n    sleep 1 # give time for the service to start\n  else\n    printf \"\\nEnable automatic daily updates for beszel-agent? (y/n): \"\n    read AUTO_UPDATE\n  fi\n  case \"$AUTO_UPDATE\" in\n  [Yy]*)\n    echo \"Setting up daily automatic updates for beszel-agent...\"\n\n    if ! crontab -u root -l 2>/dev/null | grep -q \"beszel-agent.*update\"; then\n      (crontab -u root -l 2>/dev/null; echo \"12 0 * * * /etc/init.d/beszel-agent update\") | crontab -u root -\n    fi\n\n    /etc/init.d/cron restart\n\n    printf \"\\nDaily updates have been enabled.\\n\"\n    ;;\n  esac\n\n  # Check service status\n  if ! /etc/init.d/beszel-agent running >/dev/null 2>&1; then\n    echo \"Error: The Beszel Agent service is not running.\"\n    /etc/init.d/beszel-agent status\n    exit 1\n  fi\n\nelif is_freebsd; then\n  echo \"Checking for existing FreeBSD service configuration...\"\n  # Ensure rc.d directory exists on minimal FreeBSD installs\n  mkdir -p /usr/local/etc/rc.d\n  \n  # Create environment configuration file with proper permissions if it doesn't exist\n  if [ ! -f \"$AGENT_DIR/env\" ]; then\n    echo \"Creating environment configuration file...\"\n    cat >\"$AGENT_DIR/env\" <<EOF\nLISTEN=$PORT\nKEY=\"$KEY\"\nTOKEN=$TOKEN\nHUB_URL=$HUB_URL\nEOF\n    chmod 640 \"$AGENT_DIR/env\"\n    chown root:beszel \"$AGENT_DIR/env\"\n  else\n    echo \"FreeBSD environment file already exists. Skipping creation.\"\n  fi\n  \n  # Create the rc service file if it doesn't exist\n  if [ ! -f /usr/local/etc/rc.d/beszel-agent ]; then\n    echo \"Creating FreeBSD rc service...\"\n    generate_freebsd_rc_service > /usr/local/etc/rc.d/beszel-agent\n    # Set proper permissions for the rc script\n    chmod 755 /usr/local/etc/rc.d/beszel-agent\n  else\n    echo \"FreeBSD rc service file already exists. Skipping creation.\"\n  fi\n\n  # Enable and start the service\n  echo \"Enabling and starting the agent service...\"\n  sysrc beszel_agent_enable=\"YES\"\n  service beszel-agent restart\n  \n  # Check if service started successfully\n  sleep 2\n  if ! service beszel-agent status | grep -q \"is running\"; then\n    echo \"Error: The Beszel Agent service failed to start. Checking logs...\"\n    tail -n 20 /var/log/beszel_agent.log\n    exit 1\n  fi\n\n  # Auto-update service for FreeBSD\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    AUTO_UPDATE=\"y\"\n  elif [ \"$AUTO_UPDATE_FLAG\" = \"false\" ]; then\n    AUTO_UPDATE=\"n\"\n  else\n    printf \"\\nEnable automatic daily updates for beszel-agent? (y/n): \"\n    read AUTO_UPDATE\n  fi\n  case \"$AUTO_UPDATE\" in\n  [Yy]*)\n    echo \"Setting up daily automatic updates for beszel-agent...\"\n\n    # Create cron job in /etc/cron.d \n    cat >/etc/cron.d/beszel-agent <<EOF\n# Beszel Agent daily update job\n12 0 * * * root $BIN_PATH update >/dev/null 2>&1\nEOF\n    chmod 644 /etc/cron.d/beszel-agent\n    printf \"\\nDaily updates have been enabled via /etc/cron.d.\\n\"\n    ;;\n  esac\n\n  # Check service status\n  if ! service beszel-agent status >/dev/null 2>&1; then\n    echo \"Error: The Beszel Agent service is not running.\"\n    service beszel-agent status\n    exit 1\n  fi\n\nelse\n  # Original systemd service installation code\n  if [ ! -f /etc/systemd/system/beszel-agent.service ]; then\n    echo \"Creating the systemd service for the agent...\"\n\n    # Detect NVIDIA devices and grant device permissions\n    NVIDIA_DEVICES=$(detect_nvidia_devices)\n\n    cat >/etc/systemd/system/beszel-agent.service <<EOF\n[Unit]\nDescription=Beszel Agent Service\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nEnvironment=\"PORT=$PORT\"\nEnvironment=\"KEY=$KEY\"\nEnvironment=\"TOKEN=$TOKEN\"\nEnvironment=\"HUB_URL=$HUB_URL\"\n# Environment=\"EXTRA_FILESYSTEMS=sdb\"\nExecStart=$BIN_PATH\nUser=beszel\nRestart=on-failure\nRestartSec=5\nStateDirectory=beszel-agent\n\n# Security/sandboxing settings\nKeyringMode=private\nLockPersonality=yes\nProtectClock=yes\nProtectHome=read-only\nProtectHostname=yes\nProtectKernelLogs=yes\nProtectSystem=strict\nRemoveIPC=yes\nRestrictSUIDSGID=true\n\n$(if [ -n \"$NVIDIA_DEVICES\" ]; then printf \"%b\" \"# NVIDIA device permissions\\n${NVIDIA_DEVICES}\"; fi)\n\n[Install]\nWantedBy=multi-user.target\nEOF\n  else\n    echo \"Systemd service file already exists. Skipping creation.\"\n  fi\n\n  # Load and start the service\n  printf \"\\nLoading and starting the agent service...\\n\"\n  systemctl daemon-reload\n  systemctl enable beszel-agent.service >/dev/null 2>&1\n  systemctl restart beszel-agent.service\n\n\n\n  # Prompt for auto-update setup\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    AUTO_UPDATE=\"y\"\n    sleep 1 # give time for the service to start\n  elif [ \"$AUTO_UPDATE_FLAG\" = \"false\" ]; then\n    AUTO_UPDATE=\"n\"\n    sleep 1 # give time for the service to start\n  else\n    printf \"\\nEnable automatic daily updates for beszel-agent? (y/n): \"\n    read AUTO_UPDATE\n  fi\n  case \"$AUTO_UPDATE\" in\n  [Yy]*)\n    echo \"Setting up daily automatic updates for beszel-agent...\"\n\n    # Create systemd service for the daily update\n    cat >/etc/systemd/system/beszel-agent-update.service <<EOF\n[Unit]\nDescription=Update beszel-agent if needed\nWants=beszel-agent.service\n\n[Service]\nType=oneshot\nExecStart=$BIN_PATH update\nEOF\n\n    # Create systemd timer for the daily update\n    cat >/etc/systemd/system/beszel-agent-update.timer <<EOF\n[Unit]\nDescription=Run beszel-agent update daily\n\n[Timer]\nOnCalendar=daily\nPersistent=true\nRandomizedDelaySec=4h\n\n[Install]\nWantedBy=timers.target\nEOF\n\n    systemctl daemon-reload\n    systemctl enable --now beszel-agent-update.timer >/dev/null 2>&1\n\n    printf \"\\nDaily updates have been enabled.\\n\"\n    ;;\n  esac\n\n  # Wait for the service to start or fail\n  if [ \"$(systemctl is-active beszel-agent.service)\" != \"active\" ]; then\n    echo \"Error: The Beszel Agent service is not running.\"\n    echo \"$(systemctl status beszel-agent.service)\"\n    exit 1\n  fi\nfi\n\nprintf \"\\n\\033[32mBeszel Agent has been installed successfully! It is now running on $PORT.\\033[0m\\n\"\n"
  },
  {
    "path": "supplemental/scripts/install-hub.sh",
    "content": "#!/bin/sh\n\nis_freebsd() {\n  [ \"$(uname -s)\" = \"FreeBSD\" ]\n}\n\n# Function to ensure the proxy URL ends with a /\nensure_trailing_slash() {\n  if [ -n \"$1\" ]; then\n    case \"$1\" in\n    */) echo \"$1\" ;;\n    *) echo \"$1/\" ;;\n    esac\n  else\n    echo \"$1\"\n  fi\n}\n\n# Generate FreeBSD rc service content\ngenerate_freebsd_rc_service() {\n  cat <<'EOF'\n#!/bin/sh\n\n# PROVIDE: beszel_hub\n# REQUIRE: DAEMON NETWORKING\n# BEFORE: LOGIN\n# KEYWORD: shutdown\n\n# Add the following lines to /etc/rc.conf to configure Beszel Hub:\n#\n# beszel_hub_enable (bool):   Set to YES to enable Beszel Hub\n#                             Default: YES\n# beszel_hub_port (str):      Port to listen on\n#                             Default: 8090\n# beszel_hub_user (str):      Beszel Hub daemon user\n#                             Default: beszel\n# beszel_hub_bin (str):       Path to the beszel binary\n#                             Default: /usr/local/sbin/beszel\n# beszel_hub_data (str):      Path to the beszel data directory\n#                             Default: /usr/local/etc/beszel/beszel_data\n# beszel_hub_flags (str):     Extra flags passed to beszel command invocation\n#                             Default:\n\n. /etc/rc.subr\n\nname=\"beszel_hub\"\nrcvar=beszel_hub_enable\n\nload_rc_config $name\n: ${beszel_hub_enable:=\"YES\"}\n: ${beszel_hub_port:=\"8090\"}\n: ${beszel_hub_user:=\"beszel\"}\n: ${beszel_hub_flags:=\"\"}\n: ${beszel_hub_bin:=\"/usr/local/sbin/beszel\"}\n: ${beszel_hub_data:=\"/usr/local/etc/beszel/beszel_data\"}\n\nlogfile=\"/var/log/${name}.log\"\npidfile=\"/var/run/${name}.pid\"\n\nprocname=\"/usr/sbin/daemon\"\nstart_precmd=\"${name}_prestart\"\nstart_cmd=\"${name}_start\"\nstop_cmd=\"${name}_stop\"\n\nextra_commands=\"upgrade\"\nupgrade_cmd=\"beszel_hub_upgrade\"\n\nbeszel_hub_prestart()\n{\n    if [ ! -d \"${beszel_hub_data}\" ]; then\n        echo \"Creating data directory ${beszel_hub_data}\"\n        mkdir -p \"${beszel_hub_data}\"\n        chown \"${beszel_hub_user}:${beszel_hub_user}\" \"${beszel_hub_data}\"\n    fi\n}\n\nbeszel_hub_start()\n{\n    echo \"Starting ${name}\"\n    cd \"$(dirname \"${beszel_hub_data}\")\" || exit 1\n    /usr/sbin/daemon -f \\\n            -P \"${pidfile}\" \\\n            -o \"${logfile}\" \\\n            -u \"${beszel_hub_user}\" \\\n            \"${beszel_hub_bin}\" serve --http \"0.0.0.0:${beszel_hub_port}\" ${beszel_hub_flags}\n}\n\nbeszel_hub_stop()\n{\n    pid=\"$(check_pidfile \"${pidfile}\" \"${procname}\")\"\n    if [ -n \"${pid}\" ]; then\n        echo \"Stopping ${name} (pid=${pid})\"\n        kill -- \"-${pid}\"\n        wait_for_pids \"${pid}\"\n    else\n        echo \"${name} isn't running\"\n    fi\n}\n\nbeszel_hub_upgrade()\n{\n    echo \"Upgrading ${name}\"\n    if command -v sudo >/dev/null; then\n        sudo -u \"${beszel_hub_user}\" -- \"${beszel_hub_bin}\" update\n    else\n        su -m \"${beszel_hub_user}\" -c \"${beszel_hub_bin} update\"\n    fi\n}\n\nrun_rc_command \"$1\"\nEOF\n}\n\n# Detect system architecture\ndetect_architecture() {\n  arch=$(uname -m)\n  case \"$arch\" in\n    x86_64)\n      arch=\"amd64\"\n      ;;\n    armv7l)\n      arch=\"arm\"\n      ;;\n    aarch64)\n      arch=\"arm64\"\n      ;;\n  esac\n  echo \"$arch\"\n}\n\n# Build sudo args by properly quoting everything\nbuild_sudo_args() {\n  QUOTED_ARGS=\"\"\n  while [ $# -gt 0 ]; do\n    if [ -n \"$QUOTED_ARGS\" ]; then\n      QUOTED_ARGS=\"$QUOTED_ARGS \"\n    fi\n    QUOTED_ARGS=\"$QUOTED_ARGS'$(echo \"$1\" | sed \"s/'/'\\\\\\\\''/g\")'\"\n    shift\n  done\n  echo \"$QUOTED_ARGS\"\n}\n\n# Check if running as root and re-execute with sudo if needed\nif [ \"$(id -u)\" != \"0\" ]; then\n  if command -v sudo >/dev/null 2>&1; then\n    SUDO_ARGS=$(build_sudo_args \"$@\")\n    eval \"exec sudo $0 $SUDO_ARGS\"\n  else\n    echo \"This script must be run as root. Please either:\"\n    echo \"1. Run this script as root (su root)\"\n    echo \"2. Install sudo and run with sudo\"\n    exit 1\n  fi\nfi\n\n# Define default values\nPORT=8090\nGITHUB_URL=\"https://github.com\"\nAUTO_UPDATE_FLAG=\"false\"\nUNINSTALL=false\n\n# Parse command line arguments\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    -u)\n      UNINSTALL=true\n      shift\n      ;;\n    -h|--help)\n      printf \"Beszel Hub installation script\\n\\n\"\n      printf \"Usage: ./install-hub.sh [options]\\n\\n\"\n      printf \"Options: \\n\"\n      printf \"  -u           : Uninstall the Beszel Hub\\n\"\n      printf \"  -p <port>    : Specify a port number (default: 8090)\\n\"\n      printf \"  -c, --mirror [URL] : Use a GitHub mirror/proxy URL (default: https://gh.beszel.dev)\\n\"\n      printf \"  --auto-update : Enable automatic daily updates (disabled by default)\\n\"\n      printf \"  -h, --help   : Display this help message\\n\"\n      exit 0\n      ;;\n    -p)\n      shift\n      PORT=\"$1\"\n      shift\n      ;;\n    -c | --mirror)\n      shift\n      if [ -n \"$1\" ] && ! echo \"$1\" | grep -q '^-'; then\n        GITHUB_URL=\"$(ensure_trailing_slash \"$1\")https://github.com\"\n        shift\n      else\n        GITHUB_URL=\"https://gh.beszel.dev\"\n      fi\n      ;;\n    --auto-update)\n      AUTO_UPDATE_FLAG=\"true\"\n      shift\n      ;;\n    *)\n      echo \"Invalid option: $1\" >&2\n      exit 1\n      ;;\n  esac\ndone\n\n# Set paths based on operating system\nif is_freebsd; then\n  HUB_DIR=\"/usr/local/etc/beszel\"\n  BIN_PATH=\"/usr/local/sbin/beszel\"\nelse\n  HUB_DIR=\"/opt/beszel\"\n  BIN_PATH=\"/opt/beszel/beszel\"\nfi\n\n# Uninstall process\nif [ \"$UNINSTALL\" = true ]; then\n  if is_freebsd; then\n    echo \"Stopping and disabling the Beszel Hub service...\"\n    service beszel-hub stop 2>/dev/null\n    sysrc beszel_hub_enable=\"NO\" 2>/dev/null\n\n    echo \"Removing the FreeBSD service files...\"\n    rm -f /usr/local/etc/rc.d/beszel-hub\n\n    echo \"Removing the daily update cron job...\"\n    rm -f /etc/cron.d/beszel-hub\n\n    echo \"Removing log files...\"\n    rm -f /var/log/beszel_hub.log\n\n    echo \"Removing the Beszel Hub binary and data...\"\n    rm -f \"$BIN_PATH\"\n    rm -rf \"$HUB_DIR\"\n\n    echo \"Removing the dedicated user...\"\n    pw user del beszel 2>/dev/null\n\n    echo \"The Beszel Hub has been uninstalled successfully!\"\n    exit 0\n  else\n    # Stop and disable the Beszel Hub service\n    echo \"Stopping and disabling the Beszel Hub service...\"\n    systemctl stop beszel-hub.service\n    systemctl disable beszel-hub.service\n\n    # Remove the systemd service file\n    echo \"Removing the systemd service file...\"\n    rm -f /etc/systemd/system/beszel-hub.service\n\n    # Remove the update timer and service if they exist\n    echo \"Removing the daily update service and timer...\"\n    systemctl stop beszel-hub-update.timer 2>/dev/null\n    systemctl disable beszel-hub-update.timer 2>/dev/null\n    rm -f /etc/systemd/system/beszel-hub-update.service\n    rm -f /etc/systemd/system/beszel-hub-update.timer\n\n    # Reload the systemd daemon\n    echo \"Reloading the systemd daemon...\"\n    systemctl daemon-reload\n\n    # Remove the Beszel Hub binary and data\n    echo \"Removing the Beszel Hub binary and data...\"\n    rm -rf \"$HUB_DIR\"\n\n    # Remove the dedicated user\n    echo \"Removing the dedicated user...\"\n    userdel beszel 2>/dev/null\n\n    echo \"The Beszel Hub has been uninstalled successfully!\"\n    exit 0\n  fi\nfi\n\n# Function to check if a package is installed\npackage_installed() {\n  command -v \"$1\" >/dev/null 2>&1\n}\n\n# Check for package manager and install necessary packages if not installed\nif package_installed pkg && is_freebsd; then\n  if ! package_installed tar || ! package_installed curl; then\n    pkg update\n    pkg install -y gtar curl\n  fi\nelif package_installed apt-get; then\n  if ! package_installed tar || ! package_installed curl; then\n    apt-get update\n    apt-get install -y tar curl\n  fi\nelif package_installed yum; then\n  if ! package_installed tar || ! package_installed curl; then\n    yum install -y tar curl\n  fi\nelif package_installed pacman; then\n  if ! package_installed tar || ! package_installed curl; then\n    pacman -Sy --noconfirm tar curl\n  fi\nelse\n  echo \"Warning: Please ensure 'tar' and 'curl' are installed.\"\nfi\n\n# Create a dedicated user for the service if it doesn't exist\necho \"Creating a dedicated user for the Beszel Hub service...\"\nif is_freebsd; then\n  if ! id -u beszel >/dev/null 2>&1; then\n    pw user add beszel -d /nonexistent -s /usr/sbin/nologin -c \"beszel user\"\n  fi\nelse\n  if ! id -u beszel >/dev/null 2>&1; then\n    useradd -M -s /bin/false beszel\n  fi\nfi\n\n# Create the directory for the Beszel Hub\necho \"Creating the directory for the Beszel Hub...\"\nmkdir -p \"$HUB_DIR/beszel_data\"\nchown -R beszel:beszel \"$HUB_DIR\"\nchmod 755 \"$HUB_DIR\"\n\n# Download and install the Beszel Hub\necho \"Downloading and installing the Beszel Hub...\"\n\nOS=$(uname -s | tr '[:upper:]' '[:lower:]')\nARCH=$(detect_architecture)\nFILE_NAME=\"beszel_${OS}_${ARCH}.tar.gz\"\n\nTEMP_DIR=$(mktemp -d)\nARCHIVE_PATH=\"$TEMP_DIR/$FILE_NAME\"\nDOWNLOAD_URL=\"$GITHUB_URL/henrygd/beszel/releases/latest/download/$FILE_NAME\"\n\nif ! curl -fL# --retry 3 --retry-delay 2 --connect-timeout 10 \"$DOWNLOAD_URL\" -o \"$ARCHIVE_PATH\"; then\n  echo \"Failed to download the Beszel Hub from:\"\n  echo \"$DOWNLOAD_URL\"\n  echo \"Try again with --mirror (or --mirror <url>) if GitHub is not reachable.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif ! tar -tzf \"$ARCHIVE_PATH\" >/dev/null 2>&1; then\n  echo \"Downloaded archive is invalid or incomplete (possible network/proxy issue).\"\n  echo \"Try again with --mirror (or --mirror <url>) if the download path is unstable.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif ! tar -xzf \"$ARCHIVE_PATH\" -C \"$TEMP_DIR\" beszel; then\n  echo \"Failed to extract beszel from archive.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nif [ ! -s \"$TEMP_DIR/beszel\" ]; then\n  echo \"Downloaded binary is missing or empty.\"\n  rm -rf \"$TEMP_DIR\"\n  exit 1\nfi\n\nchmod +x \"$TEMP_DIR/beszel\"\nmv \"$TEMP_DIR/beszel\" \"$BIN_PATH\"\nchown beszel:beszel \"$BIN_PATH\"\nrm -rf \"$TEMP_DIR\"\n\nif is_freebsd; then\n  echo \"Creating FreeBSD rc service...\"\n\n  # Create the rc service file\n  generate_freebsd_rc_service > /usr/local/etc/rc.d/beszel-hub\n\n  # Set proper permissions for the rc script\n  chmod 755 /usr/local/etc/rc.d/beszel-hub\n\n  # Configure the port\n  sysrc beszel_hub_port=\"$PORT\"\n\n  # Enable and start the service\n  echo \"Enabling and starting the Beszel Hub service...\"\n  sysrc beszel_hub_enable=\"YES\"\n  service beszel-hub restart\n\n  # Check if service started successfully\n  sleep 2\n  if ! service beszel-hub status | grep -q \"is running\"; then\n    echo \"Error: The Beszel Hub service failed to start. Checking logs...\"\n    tail -n 20 /var/log/beszel_hub.log\n    exit 1\n  fi\n\n  # Auto-update service for FreeBSD\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    echo \"Setting up daily automatic updates for beszel-hub...\"\n\n    # Create cron job in /etc/cron.d\n    cat >/etc/cron.d/beszel-hub <<EOF\n# Beszel Hub daily update job\n12 8 * * * root $BIN_PATH update >/dev/null 2>&1\nEOF\n    chmod 644 /etc/cron.d/beszel-hub\n    printf \"\\nDaily updates have been enabled via /etc/cron.d.\\n\"\n  fi\n\n  # Check service status\n  if ! service beszel-hub status >/dev/null 2>&1; then\n    echo \"Error: The Beszel Hub service is not running.\"\n    service beszel-hub status\n    exit 1\n  fi\n\nelse\n  # Original systemd service installation code\n  printf \"Creating the systemd service for the Beszel Hub...\\n\"\n  cat >/etc/systemd/system/beszel-hub.service <<EOF\n[Unit]\nDescription=Beszel Hub Service\nAfter=network.target\n\n[Service]\nExecStart=$BIN_PATH serve --http \"0.0.0.0:$PORT\"\nWorkingDirectory=$HUB_DIR\nUser=beszel\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  # Load and start the service\n  printf \"Loading and starting the Beszel Hub service...\\n\"\n  systemctl daemon-reload\n  systemctl enable --quiet beszel-hub.service\n  systemctl start --quiet beszel-hub.service\n\n  # Wait for the service to start or fail\n  sleep 2\n\n  # Check if the service is running\n  if [ \"$(systemctl is-active beszel-hub.service)\" != \"active\" ]; then\n    echo \"Error: The Beszel Hub service is not running.\"\n    echo \"$(systemctl status beszel-hub.service)\"\n    exit 1\n  fi\n\n  # Enable auto-update if flag is set to true\n  if [ \"$AUTO_UPDATE_FLAG\" = \"true\" ]; then\n    echo \"Setting up daily automatic updates for beszel-hub...\"\n\n    # Create systemd service for the daily update\n    cat >/etc/systemd/system/beszel-hub-update.service <<EOF\n[Unit]\nDescription=Update beszel-hub if needed\nWants=beszel-hub.service\n\n[Service]\nType=oneshot\nExecStart=$BIN_PATH update\nEOF\n\n    # Create systemd timer for the daily update\n    cat >/etc/systemd/system/beszel-hub-update.timer <<EOF\n[Unit]\nDescription=Run beszel-hub update daily\n\n[Timer]\nOnCalendar=daily\nPersistent=true\nRandomizedDelaySec=4h\n\n[Install]\nWantedBy=timers.target\nEOF\n\n    systemctl daemon-reload\n    systemctl enable --now beszel-hub-update.timer\n\n    printf \"\\nDaily updates have been enabled.\\n\"\n  fi\nfi\n\nprintf \"\\n\\033[32mBeszel Hub has been installed successfully! It is now accessible on port $PORT.\\033[0m\\n\"\n"
  },
  {
    "path": "supplemental/scripts/upgrade-agent-wrapper.ps1",
    "content": "param (\n    [switch]$Elevated\n)\n\n# Beszel Agent Upgrade Wrapper\n# This script downloads and executes the latest upgrade script from GitHub\n\n$ErrorActionPreference = \"Stop\"\n\ntry {\n    Write-Host \"Beszel Agent Upgrade Wrapper\" -ForegroundColor Cyan\n    Write-Host \"============================\" -ForegroundColor Cyan\n    Write-Host \"\"\n    \n    # Define the URL for the latest upgrade script\n    $scriptUrl = \"https://raw.githubusercontent.com/henrygd/beszel/main/supplemental/scripts/upgrade-agent.ps1\"\n    $tempScriptPath = \"$env:TEMP\\beszel-upgrade-agent-$(Get-Date -Format 'yyyyMMdd-HHmmss').ps1\"\n    \n    Write-Host \"Downloading latest upgrade script...\" -ForegroundColor Yellow\n    Write-Host \"From: $scriptUrl\"\n    Write-Host \"To: $tempScriptPath\"\n    \n    # Download the latest upgrade script\n    try {\n        Invoke-WebRequest -Uri $scriptUrl -OutFile $tempScriptPath -UseBasicParsing\n        Write-Host \"Download completed successfully.\" -ForegroundColor Green\n    }\n    catch {\n        Write-Host \"Failed to download upgrade script: $($_.Exception.Message)\" -ForegroundColor Red\n        Write-Host \"Please check your internet connection and try again.\" -ForegroundColor Red\n        exit 1\n    }\n    \n    # Verify the script was downloaded\n    if (-not (Test-Path $tempScriptPath)) {\n        Write-Host \"ERROR: Downloaded script not found at $tempScriptPath\" -ForegroundColor Red\n        exit 1\n    }\n    \n    Write-Host \"\"\n    Write-Host \"Executing upgrade script...\" -ForegroundColor Yellow\n    \n    # Execute the downloaded script with the same parameters\n    if ($Elevated) {\n        & $tempScriptPath -Elevated\n    } else {\n        & $tempScriptPath\n    }\n    \n    $scriptExitCode = $LASTEXITCODE\n    \n    Write-Host \"\"\n    Write-Host \"Cleaning up temporary files...\" -ForegroundColor Yellow\n    \n    # Clean up the temporary script\n    try {\n        Remove-Item $tempScriptPath -Force -ErrorAction SilentlyContinue\n        Write-Host \"Cleanup completed.\" -ForegroundColor Green\n    }\n    catch {\n        Write-Host \"Warning: Could not remove temporary script: $tempScriptPath\" -ForegroundColor Yellow\n    }\n    \n    # Exit with the same code as the upgrade script\n    exit $scriptExitCode\n}\ncatch {\n    Write-Host \"ERROR: $($_.Exception.Message)\" -ForegroundColor Red\n    Write-Host \"Upgrade wrapper failed. Please check the error message above.\" -ForegroundColor Red\n    \n    # Clean up on error\n    if ($tempScriptPath -and (Test-Path $tempScriptPath)) {\n        try {\n            Remove-Item $tempScriptPath -Force -ErrorAction SilentlyContinue\n        }\n        catch {\n            # Ignore cleanup errors\n        }\n    }\n    \n    exit 1\n} "
  },
  {
    "path": "supplemental/scripts/upgrade-agent.ps1",
    "content": "param (\n    [switch]$Elevated\n)\n\n# Stop on first error\n$ErrorActionPreference = \"Stop\"\n\n#region Utility Functions\n\n# Function to check if running as admin\nfunction Test-Admin {\n    return ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)\n}\n\n# Function to check if a command exists\nfunction Test-CommandExists {\n    param (\n        [Parameter(Mandatory=$true)]\n        [string]$Command\n    )\n    return (Get-Command $Command -ErrorAction SilentlyContinue)\n}\n\n# Function to find beszel-agent in common installation locations\nfunction Find-BeszelAgent {\n    # First check if it's in PATH\n    $agentCmd = Get-Command \"beszel-agent\" -ErrorAction SilentlyContinue\n    if ($agentCmd) {\n        return $agentCmd.Source\n    }\n    \n    # Common installation paths to check\n    $commonPaths = @(\n        \"$env:USERPROFILE\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\",\n        \"$env:ProgramData\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\",\n        \"$env:LOCALAPPDATA\\Microsoft\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"$env:ProgramFiles\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"${env:ProgramFiles(x86)}\\WinGet\\Packages\\henrygd.beszel-agent*\\beszel-agent.exe\",\n        \"$env:ProgramFiles\\beszel-agent\\beszel-agent.exe\",\n        \"$env:ProgramFiles(x86)\\beszel-agent\\beszel-agent.exe\",\n        \"$env:SystemDrive\\Users\\*\\scoop\\apps\\beszel-agent\\current\\beszel-agent.exe\"\n    )\n    \n    foreach ($path in $commonPaths) {\n        # Handle wildcard paths\n        if ($path.Contains(\"*\")) {\n            $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue\n            if ($foundPaths) {\n                return $foundPaths[0].FullName\n            }\n        } else {\n            if (Test-Path $path) {\n                return $path\n            }\n        }\n    }\n    \n    return $null\n}\n\n# Function to find NSSM in common installation locations\nfunction Find-NSSM {\n    # First check if it's in PATH\n    $nssmCmd = Get-Command \"nssm\" -ErrorAction SilentlyContinue\n    if ($nssmCmd) {\n        return $nssmCmd.Source\n    }\n    \n    # Common installation paths to check\n    $commonPaths = @(\n        \"$env:USERPROFILE\\scoop\\apps\\nssm\\current\\nssm.exe\",\n        \"$env:ProgramData\\scoop\\apps\\nssm\\current\\nssm.exe\",\n        \"$env:LOCALAPPDATA\\Microsoft\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"$env:ProgramFiles\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"${env:ProgramFiles(x86)}\\WinGet\\Packages\\NSSM.NSSM*\\nssm.exe\",\n        \"$env:SystemDrive\\Users\\*\\scoop\\apps\\nssm\\current\\nssm.exe\"\n    )\n    \n    foreach ($path in $commonPaths) {\n        # Handle wildcard paths\n        if ($path.Contains(\"*\")) {\n            $foundPaths = Get-ChildItem -Path $path -ErrorAction SilentlyContinue\n            if ($foundPaths) {\n                return $foundPaths[0].FullName\n            }\n        } else {\n            if (Test-Path $path) {\n                return $path\n            }\n        }\n    }\n    \n    return $null\n}\n\n#endregion\n\n#region Upgrade Functions\n\n# Function to upgrade beszel-agent with Scoop\nfunction Upgrade-BeszelAgentWithScoop {\n    Write-Host \"Upgrading beszel-agent with Scoop...\"\n    scoop update beszel-agent\n    \n    if (-not (Test-CommandExists \"beszel-agent\")) {\n        throw \"Failed to upgrade beszel-agent with Scoop\"\n    }\n    \n    return $(Join-Path -Path $(scoop prefix beszel-agent) -ChildPath \"beszel-agent.exe\")\n}\n\n# Function to upgrade beszel-agent with WinGet\nfunction Upgrade-BeszelAgentWithWinGet {\n    Write-Host \"Upgrading beszel-agent with WinGet...\"\n    \n    # Temporarily change ErrorActionPreference to allow WinGet to complete and show output\n    $originalErrorActionPreference = $ErrorActionPreference\n    $ErrorActionPreference = \"Continue\"\n    \n    # Use call operator (&) and capture exit code properly\n    & winget upgrade --exact --id henrygd.beszel-agent --accept-source-agreements --accept-package-agreements | Out-Null\n    $wingetExitCode = $LASTEXITCODE\n    \n    # Restore original ErrorActionPreference\n    $ErrorActionPreference = $originalErrorActionPreference\n    \n    # WinGet exit codes:\n    # 0 = Success\n    # -1978335212 (0x8A150014) = No applicable upgrade found (package is up to date)\n    # -1978335189 (0x8A15002B) = Another \"no upgrade needed\" variant\n    # Other codes indicate actual errors\n    if ($wingetExitCode -eq -1978335212 -or $wingetExitCode -eq -1978335189) {\n        Write-Host \"Package is already up to date.\" -ForegroundColor Green\n    } elseif ($wingetExitCode -ne 0)  {\n        Write-Host \"WinGet exit code: $wingetExitCode\" -ForegroundColor Yellow\n    }\n    \n    # Refresh PATH environment variable to make beszel-agent available in current session\n    $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\", \"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\", \"User\")\n    \n    # Find the path to the beszel-agent executable\n    $agentPath = (Get-Command beszel-agent -ErrorAction SilentlyContinue).Source\n    \n    if (-not $agentPath) {\n        # Try to find it using our search function\n        $agentPath = Find-BeszelAgent\n        if (-not $agentPath) {\n            throw \"Could not find beszel-agent executable path after upgrade\"\n        }\n    }\n    \n    return $agentPath\n}\n\n# Function to get current service configuration\nfunction Get-ServiceConfiguration {\n    param (\n        [string]$NSSMPath = \"\"\n    )\n    \n    # Determine the NSSM executable to use\n    $nssmCommand = \"nssm\"\n    if ($NSSMPath -and (Test-Path $NSSMPath)) {\n        $nssmCommand = $NSSMPath\n    } elseif (-not (Test-CommandExists \"nssm\")) {\n        throw \"NSSM is not available in PATH and no valid NSSMPath was provided\"\n    }\n    \n    # Check if service exists\n    $existingService = Get-Service -Name \"beszel-agent\" -ErrorAction SilentlyContinue\n    if (-not $existingService) {\n        throw \"beszel-agent service does not exist. Please run the installation script first.\"\n    }\n    \n    # Get current service configuration\n    $config = @{}\n    \n    try {\n        # Get current application path\n        $currentPath = & $nssmCommand get beszel-agent Application\n        if ($LASTEXITCODE -eq 0) {\n            $config.CurrentPath = $currentPath.Trim()\n        }\n        \n        # Get environment variables\n        $envVars = & $nssmCommand get beszel-agent AppEnvironmentExtra\n        if ($LASTEXITCODE -eq 0 -and $envVars) {\n            $config.EnvironmentVars = $envVars\n        }\n        \n        Write-Host \"Current service configuration retrieved successfully.\"\n        Write-Host \"Current agent path: $($config.CurrentPath)\"\n        \n        return $config\n    }\n    catch {\n        throw \"Failed to retrieve current service configuration: $($_.Exception.Message)\"\n    }\n}\n\n# Function to update service path\nfunction Update-ServicePath {\n    param (\n        [Parameter(Mandatory=$true)]\n        [string]$NewAgentPath,\n        [string]$NSSMPath = \"\"\n    )\n    \n    Write-Host \"Updating beszel-agent service path...\"\n    \n    # Determine the NSSM executable to use\n    $nssmCommand = \"nssm\"\n    if ($NSSMPath -and (Test-Path $NSSMPath)) {\n        $nssmCommand = $NSSMPath\n        Write-Host \"Using NSSM from: $NSSMPath\"\n    } elseif (-not (Test-CommandExists \"nssm\")) {\n        throw \"NSSM is not available in PATH and no valid NSSMPath was provided\"\n    }\n    \n    # Update the application path\n    & $nssmCommand set beszel-agent Application $NewAgentPath\n    if ($LASTEXITCODE -ne 0) {\n        throw \"Failed to update beszel-agent service path\"\n    }\n    \n    Write-Host \"Service path updated to: $NewAgentPath\"\n    \n    # Start the service\n    Start-BeszelAgentService -NSSMPath $nssmCommand\n}\n\n# Function to start and monitor the service\nfunction Start-BeszelAgentService {\n    param (\n        [string]$NSSMPath = \"\"\n    )\n    \n    Write-Host \"Starting beszel-agent service...\"\n    \n    # Determine the NSSM executable to use\n    $nssmCommand = \"nssm\"\n    if ($NSSMPath -and (Test-Path $NSSMPath)) {\n        $nssmCommand = $NSSMPath\n    } elseif (-not (Test-CommandExists \"nssm\")) {\n        throw \"NSSM is not available in PATH and no valid NSSMPath was provided\"\n    }\n    \n    & $nssmCommand start beszel-agent\n    $startResult = $LASTEXITCODE\n    \n    # Only enter the status check loop if the NSSM start command failed\n    if ($startResult -ne 0) {\n        Write-Host \"NSSM start command returned error code: $startResult\" -ForegroundColor Yellow\n        Write-Host \"This could be due to 'SERVICE_START_PENDING' state. Checking service status...\"\n        \n        # Allow up to 10 seconds for the service to start, checking every second\n        $maxWaitTime = 10 # seconds\n        $elapsedTime = 0\n        $serviceStarted = $false\n        \n        while (-not $serviceStarted -and $elapsedTime -lt $maxWaitTime) {\n            Start-Sleep -Seconds 1\n            $elapsedTime += 1\n\n            $serviceStatus = & $nssmCommand status beszel-agent\n            \n            if ($serviceStatus -eq \"SERVICE_RUNNING\") {\n                $serviceStarted = $true\n                Write-Host \"Success! The beszel-agent service is now running.\" -ForegroundColor Green\n            }\n            elseif ($serviceStatus -like \"*PENDING*\") {\n                Write-Host \"Service is still starting (status: $serviceStatus)... waiting\" -ForegroundColor Yellow\n            }\n            else {\n                Write-Host \"Warning: The service status is '$serviceStatus' instead of 'SERVICE_RUNNING'.\" -ForegroundColor Yellow\n                Write-Host \"You may need to troubleshoot the service installation.\" -ForegroundColor Yellow\n                break\n            }\n        }\n        \n        if (-not $serviceStarted) {\n            Write-Host \"Service did not reach running state.\" -ForegroundColor Yellow\n            Write-Host \"You can check status manually with 'nssm status beszel-agent'\" -ForegroundColor Yellow\n        }\n    } else {\n        # NSSM start command was successful\n        Write-Host \"Success! The beszel-agent service is running properly.\" -ForegroundColor Green\n    }\n}\n\n#endregion\n\n#region Main Script Execution\n\n# Check if we're running as admin\n$isAdmin = Test-Admin\n\ntry {\n    Write-Host \"Beszel Agent Upgrade Script\" -ForegroundColor Cyan\n    Write-Host \"===========================\" -ForegroundColor Cyan\n    \n    # First: Check if service exists (doesn't require admin)\n    $existingService = Get-Service -Name \"beszel-agent\" -ErrorAction SilentlyContinue\n    if (-not $existingService) {\n        Write-Host \"ERROR: beszel-agent service does not exist.\" -ForegroundColor Red\n        Write-Host \"Please run the installation script first before attempting to upgrade.\" -ForegroundColor Red\n        exit 1\n    }\n    \n    # Find current NSSM and agent paths\n    $nssmPath = Find-NSSM\n    if (-not $nssmPath -and (Test-CommandExists \"nssm\")) {\n        $nssmPath = (Get-Command \"nssm\" -ErrorAction SilentlyContinue).Source\n    }\n    \n    if (-not $nssmPath) {\n        Write-Host \"ERROR: NSSM not found. Cannot manage the service without NSSM.\" -ForegroundColor Red\n        exit 1\n    }\n    \n    # Get current service configuration (doesn't require admin)\n    Write-Host \"Retrieving current service configuration...\"\n    $currentConfig = Get-ServiceConfiguration -NSSMPath $nssmPath\n    \n    # Stop the service before upgrade\n    Write-Host \"Stopping beszel-agent service...\"\n    & $nssmPath stop beszel-agent\n    if ($LASTEXITCODE -ne 0) {\n        Write-Host \"Warning: Failed to stop service, continuing anyway...\" -ForegroundColor Yellow\n    }\n    \n    # Upgrade the agent (doesn't require admin)\n    Write-Host \"Upgrading beszel-agent...\"\n    $newAgentPath = $null\n    \n    if (Test-CommandExists \"scoop\") {\n        Write-Host \"Using Scoop for upgrade...\"\n        $newAgentPath = Upgrade-BeszelAgentWithScoop\n    }\n    elseif (Test-CommandExists \"winget\") {\n        Write-Host \"Using WinGet for upgrade...\"\n        $newAgentPath = Upgrade-BeszelAgentWithWinGet\n    }\n    else {\n        Write-Host \"ERROR: Neither Scoop nor WinGet is available for upgrading.\" -ForegroundColor Red\n        exit 1\n    }\n    \n    if (-not $newAgentPath) {\n        $newAgentPath = Find-BeszelAgent\n        if (-not $newAgentPath) {\n            throw \"Could not find beszel-agent executable after upgrade.\"\n        }\n    }\n    \n    Write-Host \"New agent path: $newAgentPath\"\n    \n    # Check if the path has changed\n    if ($currentConfig.CurrentPath -eq $newAgentPath) {\n        Write-Host \"Agent path has not changed. Restarting service...\" -ForegroundColor Green\n        Start-BeszelAgentService -NSSMPath $nssmPath\n        Write-Host \"Upgrade completed successfully!\" -ForegroundColor Green\n        exit 0\n    }\n    \n    Write-Host \"Agent path has changed from:\" -ForegroundColor Yellow\n    Write-Host \"  Old: $($currentConfig.CurrentPath)\" -ForegroundColor Yellow\n    Write-Host \"  New: $newAgentPath\" -ForegroundColor Yellow\n    Write-Host \"\"\n    \n    # If we need admin rights for service update and we don't have them, relaunch\n    if (-not $isAdmin -and -not $Elevated) {\n        Write-Host \"Admin privileges required for service path update. Relaunching as admin...\" -ForegroundColor Yellow\n        \n        # Prepare arguments for the elevated script\n        $argumentList = @(\n            \"-ExecutionPolicy\", \"Bypass\",\n            \"-File\", \"`\"$PSCommandPath`\"\",\n            \"-Elevated\"\n        )\n        \n        # Relaunch the script with the -Elevated switch\n        Start-Process powershell.exe -Verb RunAs -ArgumentList $argumentList\n        exit\n    }\n    \n    # Update service path (requires admin)\n    if ($isAdmin -or $Elevated) {\n        Update-ServicePath -NewAgentPath $newAgentPath -NSSMPath $nssmPath\n        \n        Write-Host \"\"\n        Write-Host \"Upgrade completed successfully!\" -ForegroundColor Green\n        Write-Host \"The beszel-agent service has been updated to use the new executable path.\" -ForegroundColor Green\n        \n        # Pause to see results if this is an elevated window\n        if ($Elevated) {\n            Write-Host \"\"\n            Write-Host \"Press any key to exit...\" -ForegroundColor Cyan\n            $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n        }\n    }\n}\ncatch {\n    Write-Host \"ERROR: $($_.Exception.Message)\" -ForegroundColor Red\n    Write-Host \"Upgrade failed. Please check the error message above.\" -ForegroundColor Red\n    \n    # Pause if this is likely a new window\n    if ($Elevated -or (-not $isAdmin)) {\n        Write-Host \"Press any key to exit...\" -ForegroundColor Red\n        $null = $Host.UI.RawUI.ReadKey(\"NoEcho,IncludeKeyDown\")\n    }\n    exit 1\n}\n\n#endregion "
  }
]