mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-05 04:21:50 +02:00
Compare commits
1 Commits
1e3a44e05d
...
apple-gpu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c561aef409 |
1
Makefile
1
Makefile
@@ -51,6 +51,7 @@ clean:
|
|||||||
lint:
|
lint:
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
|
test: export GOEXPERIMENT=synctest
|
||||||
test:
|
test:
|
||||||
go test -tags=testing ./...
|
go test -tags=testing ./...
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
@@ -29,7 +28,6 @@ import (
|
|||||||
// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)
|
// ansiEscapePattern matches ANSI escape sequences (colors, cursor movement, etc.)
|
||||||
// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K
|
// This includes CSI sequences like \x1b[...m and simple escapes like \x1b[K
|
||||||
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
|
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[@-Z\\-_]`)
|
||||||
var dockerContainerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Docker API timeout in milliseconds
|
// Docker API timeout in milliseconds
|
||||||
@@ -651,34 +649,9 @@ func getDockerHost() string {
|
|||||||
return scheme + socks[0]
|
return scheme + socks[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateContainerID(containerID string) error {
|
|
||||||
if !dockerContainerIDPattern.MatchString(containerID) {
|
|
||||||
return fmt.Errorf("invalid container id")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildDockerContainerEndpoint(containerID, action string, query url.Values) (string, error) {
|
|
||||||
if err := validateContainerID(containerID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: "localhost",
|
|
||||||
Path: fmt.Sprintf("/containers/%s/%s", url.PathEscape(containerID), action),
|
|
||||||
}
|
|
||||||
if len(query) > 0 {
|
|
||||||
u.RawQuery = query.Encode()
|
|
||||||
}
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getContainerInfo fetches the inspection data for a container
|
// getContainerInfo fetches the inspection data for a container
|
||||||
func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID string) ([]byte, error) {
|
func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID string) ([]byte, error) {
|
||||||
endpoint, err := buildDockerContainerEndpoint(containerID, "json", nil)
|
endpoint := fmt.Sprintf("http://localhost/containers/%s/json", containerID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -709,15 +682,7 @@ func (dm *dockerManager) getContainerInfo(ctx context.Context, containerID strin
|
|||||||
|
|
||||||
// getLogs fetches the logs for a container
|
// getLogs fetches the logs for a container
|
||||||
func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {
|
func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (string, error) {
|
||||||
query := url.Values{
|
endpoint := fmt.Sprintf("http://localhost/containers/%s/logs?stdout=1&stderr=1&tail=%d", containerID, dockerLogsTail)
|
||||||
"stdout": []string{"1"},
|
|
||||||
"stderr": []string{"1"},
|
|
||||||
"tail": []string{fmt.Sprintf("%d", dockerLogsTail)},
|
|
||||||
}
|
|
||||||
endpoint, err := buildDockerContainerEndpoint(containerID, "logs", query)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -735,17 +700,8 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
contentType := resp.Header.Get("Content-Type")
|
multiplexed := resp.Header.Get("Content-Type") == "application/vnd.docker.multiplexed-stream"
|
||||||
multiplexed := strings.HasSuffix(contentType, "multiplexed-stream")
|
if err := decodeDockerLogStream(resp.Body, &builder, multiplexed); err != nil {
|
||||||
logReader := io.Reader(resp.Body)
|
|
||||||
if !multiplexed {
|
|
||||||
// Podman may return multiplexed logs without Content-Type. Sniff the first frame header
|
|
||||||
// with a small buffered reader only when the header check fails.
|
|
||||||
bufferedReader := bufio.NewReaderSize(resp.Body, 8)
|
|
||||||
multiplexed = detectDockerMultiplexedStream(bufferedReader)
|
|
||||||
logReader = bufferedReader
|
|
||||||
}
|
|
||||||
if err := decodeDockerLogStream(logReader, &builder, multiplexed); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,23 +713,6 @@ func (dm *dockerManager) getLogs(ctx context.Context, containerID string) (strin
|
|||||||
return logs, nil
|
return logs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectDockerMultiplexedStream(reader *bufio.Reader) bool {
|
|
||||||
const headerSize = 8
|
|
||||||
header, err := reader.Peek(headerSize)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if header[0] != 0x01 && header[0] != 0x02 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Docker's stream framing header reserves bytes 1-3 as zero.
|
|
||||||
if header[1] != 0 || header[2] != 0 || header[3] != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
frameLen := binary.BigEndian.Uint32(header[4:])
|
|
||||||
return frameLen <= maxLogFrameSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder, multiplexed bool) error {
|
func decodeDockerLogStream(reader io.Reader, builder *strings.Builder, multiplexed bool) error {
|
||||||
if !multiplexed {
|
if !multiplexed {
|
||||||
_, err := io.Copy(builder, io.LimitReader(reader, maxTotalLogSize))
|
_, err := io.Copy(builder, io.LimitReader(reader, maxTotalLogSize))
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -26,37 +25,6 @@ import (
|
|||||||
|
|
||||||
var defaultCacheTimeMs = uint16(60_000)
|
var defaultCacheTimeMs = uint16(60_000)
|
||||||
|
|
||||||
type recordingRoundTripper struct {
|
|
||||||
statusCode int
|
|
||||||
body string
|
|
||||||
contentType string
|
|
||||||
called bool
|
|
||||||
lastPath string
|
|
||||||
lastQuery map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
rt.called = true
|
|
||||||
rt.lastPath = req.URL.EscapedPath()
|
|
||||||
rt.lastQuery = map[string]string{}
|
|
||||||
for key, values := range req.URL.Query() {
|
|
||||||
if len(values) > 0 {
|
|
||||||
rt.lastQuery[key] = values[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp := &http.Response{
|
|
||||||
StatusCode: rt.statusCode,
|
|
||||||
Status: "200 OK",
|
|
||||||
Header: make(http.Header),
|
|
||||||
Body: io.NopCloser(strings.NewReader(rt.body)),
|
|
||||||
Request: req,
|
|
||||||
}
|
|
||||||
if rt.contentType != "" {
|
|
||||||
resp.Header.Set("Content-Type", rt.contentType)
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cycleCpuDeltas cycles the CPU tracking data for a specific cache time interval
|
// cycleCpuDeltas cycles the CPU tracking data for a specific cache time interval
|
||||||
func (dm *dockerManager) cycleCpuDeltas(cacheTimeMs uint16) {
|
func (dm *dockerManager) cycleCpuDeltas(cacheTimeMs uint16) {
|
||||||
// Clear the CPU tracking maps for this cache time interval
|
// Clear the CPU tracking maps for this cache time interval
|
||||||
@@ -148,72 +116,6 @@ func TestCalculateMemoryUsage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildDockerContainerEndpoint(t *testing.T) {
|
|
||||||
t.Run("valid container ID builds escaped endpoint", func(t *testing.T) {
|
|
||||||
endpoint, err := buildDockerContainerEndpoint("0123456789ab", "json", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "http://localhost/containers/0123456789ab/json", endpoint)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid container ID is rejected", func(t *testing.T) {
|
|
||||||
_, err := buildDockerContainerEndpoint("../../version", "json", nil)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid container id")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainerDetailsRequestsValidateContainerID(t *testing.T) {
|
|
||||||
rt := &recordingRoundTripper{
|
|
||||||
statusCode: 200,
|
|
||||||
body: `{"Config":{"Env":["SECRET=1"]}}`,
|
|
||||||
}
|
|
||||||
dm := &dockerManager{
|
|
||||||
client: &http.Client{Transport: rt},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := dm.getContainerInfo(context.Background(), "../version")
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "invalid container id")
|
|
||||||
assert.False(t, rt.called, "request should be rejected before dispatching to Docker API")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainerDetailsRequestsUseExpectedDockerPaths(t *testing.T) {
|
|
||||||
t.Run("container info uses container json endpoint", func(t *testing.T) {
|
|
||||||
rt := &recordingRoundTripper{
|
|
||||||
statusCode: 200,
|
|
||||||
body: `{"Config":{"Env":["SECRET=1"]},"Name":"demo"}`,
|
|
||||||
}
|
|
||||||
dm := &dockerManager{
|
|
||||||
client: &http.Client{Transport: rt},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := dm.getContainerInfo(context.Background(), "0123456789ab")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, rt.called)
|
|
||||||
assert.Equal(t, "/containers/0123456789ab/json", rt.lastPath)
|
|
||||||
assert.NotContains(t, string(body), "SECRET=1", "sensitive env vars should be removed")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("container logs uses expected endpoint and query params", func(t *testing.T) {
|
|
||||||
rt := &recordingRoundTripper{
|
|
||||||
statusCode: 200,
|
|
||||||
body: "line1\nline2\n",
|
|
||||||
}
|
|
||||||
dm := &dockerManager{
|
|
||||||
client: &http.Client{Transport: rt},
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := dm.getLogs(context.Background(), "abcdef123456")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, rt.called)
|
|
||||||
assert.Equal(t, "/containers/abcdef123456/logs", rt.lastPath)
|
|
||||||
assert.Equal(t, "1", rt.lastQuery["stdout"])
|
|
||||||
assert.Equal(t, "1", rt.lastQuery["stderr"])
|
|
||||||
assert.Equal(t, "200", rt.lastQuery["tail"])
|
|
||||||
assert.Equal(t, "line1\nline2\n", logs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateCpuPercentage(t *testing.T) {
|
func TestValidateCpuPercentage(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -914,42 +816,6 @@ func TestContainerStatsEndToEndWithRealData(t *testing.T) {
|
|||||||
assert.Equal(t, testTime, testStats.PrevReadTime)
|
assert.Equal(t, testTime, testStats.PrevReadTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLogsDetectsMultiplexedWithoutContentType(t *testing.T) {
|
|
||||||
// Docker multiplexed frame: [stream][0,0,0][len(4 bytes BE)][payload]
|
|
||||||
frame := []byte{
|
|
||||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
|
||||||
'H', 'e', 'l', 'l', 'o',
|
|
||||||
}
|
|
||||||
rt := &recordingRoundTripper{
|
|
||||||
statusCode: 200,
|
|
||||||
body: string(frame),
|
|
||||||
// Intentionally omit content type to simulate Podman behavior.
|
|
||||||
}
|
|
||||||
dm := &dockerManager{
|
|
||||||
client: &http.Client{Transport: rt},
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := dm.getLogs(context.Background(), "abcdef123456")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "Hello", logs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetLogsDoesNotMisclassifyRawStreamAsMultiplexed(t *testing.T) {
|
|
||||||
// Starts with 0x01, but doesn't match Docker frame signature (reserved bytes aren't all zero).
|
|
||||||
raw := []byte{0x01, 0x02, 0x03, 0x04, 'r', 'a', 'w'}
|
|
||||||
rt := &recordingRoundTripper{
|
|
||||||
statusCode: 200,
|
|
||||||
body: string(raw),
|
|
||||||
}
|
|
||||||
dm := &dockerManager{
|
|
||||||
client: &http.Client{Transport: rt},
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := dm.getLogs(context.Background(), "abcdef123456")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, raw, []byte(logs))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEdgeCasesWithRealData(t *testing.T) {
|
func TestEdgeCasesWithRealData(t *testing.T) {
|
||||||
// Test with minimal container stats
|
// Test with minimal container stats
|
||||||
minimalStats := &container.ApiStats{
|
minimalStats := &container.ApiStats{
|
||||||
|
|||||||
10
agent/gpu.go
10
agent/gpu.go
@@ -708,16 +708,8 @@ func (gm *GPUManager) resolveLegacyCollectorPriority(caps gpuCapabilities) []col
|
|||||||
priorities = append(priorities, collectorSourceIntelGpuTop)
|
priorities = append(priorities, collectorSourceIntelGpuTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apple collectors are currently opt-in only for testing.
|
// Apple collectors are currently opt-in only.
|
||||||
// Enable them with GPU_COLLECTOR=macmon or GPU_COLLECTOR=powermetrics.
|
// Enable them with GPU_COLLECTOR=macmon or GPU_COLLECTOR=powermetrics.
|
||||||
// TODO: uncomment below when Apple collectors are confirmed to be working.
|
|
||||||
//
|
|
||||||
// Prefer macmon on macOS (no sudo). Fall back to powermetrics if present.
|
|
||||||
// if caps.hasMacmon {
|
|
||||||
// priorities = append(priorities, collectorSourceMacmon)
|
|
||||||
// } else if caps.hasPowermetrics {
|
|
||||||
// priorities = append(priorities, collectorSourcePowermetrics)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Keep nvtop as a last resort only when no vendor collector exists.
|
// Keep nvtop as a last resort only when no vendor collector exists.
|
||||||
if len(priorities) == 0 && caps.hasNvtop {
|
if len(priorities) == 0 && caps.hasNvtop {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func TestHealth(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// This test uses synctest to simulate time passing.
|
// This test uses synctest to simulate time passing.
|
||||||
|
// NOTE: This test requires GOEXPERIMENT=synctest to run.
|
||||||
t.Run("check with simulated time", func(t *testing.T) {
|
t.Run("check with simulated time", func(t *testing.T) {
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
// Update the file to set the initial timestamp.
|
// Update the file to set the initial timestamp.
|
||||||
|
|||||||
30
go.mod
30
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/henrygd/beszel
|
module github.com/henrygd/beszel
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
@@ -11,17 +11,17 @@ require (
|
|||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lxzan/gws v1.8.9
|
github.com/lxzan/gws v1.8.9
|
||||||
github.com/nicholas-fedor/shoutrrr v0.13.2
|
github.com/nicholas-fedor/shoutrrr v0.13.1
|
||||||
github.com/pocketbase/dbx v1.12.0
|
github.com/pocketbase/dbx v1.11.0
|
||||||
github.com/pocketbase/pocketbase v0.36.4
|
github.com/pocketbase/pocketbase v0.36.2
|
||||||
github.com/shirou/gopsutil/v4 v4.26.1
|
github.com/shirou/gopsutil/v4 v4.26.1
|
||||||
github.com/spf13/cast v1.10.0
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.47.0
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.40.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.3 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
@@ -54,15 +54,15 @@ require (
|
|||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/image v0.36.0 // indirect
|
golang.org/x/image v0.35.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/oauth2 v0.35.0 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/term v0.40.0 // indirect
|
golang.org/x/term v0.39.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
howett.net/plist v1.0.1 // indirect
|
howett.net/plist v1.0.1 // indirect
|
||||||
modernc.org/libc v1.67.6 // indirect
|
modernc.org/libc v1.67.6 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.45.0 // indirect
|
modernc.org/sqlite v1.44.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
76
go.sum
76
go.sum
@@ -69,14 +69,14 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
|||||||
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
||||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
|
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
|
||||||
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
|
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
@@ -85,19 +85,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.13.2 h1:hfsYBIqSFYGg92pZP5CXk/g7/OJIkLYmiUnRl+AD1IA=
|
github.com/nicholas-fedor/shoutrrr v0.13.1 h1:llEoHNbnMM4GfQ9+2Ns3n6ssvNfi3NPWluM0AQiicoY=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.13.2/go.mod h1:ZqzV3gY/Wj6AvWs1etlO7+yKbh4iptSbeL8avBpMQbA=
|
github.com/nicholas-fedor/shoutrrr v0.13.1/go.mod h1:kU4cFJpEAtTzl3iV0l+XUXmM90OlC5T01b7roM4/pYM=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
|
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
||||||
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||||
github.com/pocketbase/pocketbase v0.36.4 h1:zTjRZbp2WfTOJJfb+pFRWa200UaQwxZYt8RzkFMlAZ4=
|
github.com/pocketbase/pocketbase v0.36.2 h1:mzrxnvXKc3yxKlvZdbwoYXkH8kfIETteD0hWdgj0VI4=
|
||||||
github.com/pocketbase/pocketbase v0.36.4/go.mod h1:9CiezhRudd9FZGa5xZa53QZBTNxc5vvw/FGG+diAECI=
|
github.com/pocketbase/pocketbase v0.36.2/go.mod h1:71vSF8whUDzC8mcLFE10+Qatf9JQdeOGIRWawOuLLKM=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -129,20 +129,20 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -150,20 +150,20 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -195,8 +195,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=
|
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||||
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -42,8 +41,6 @@ type Hub struct {
|
|||||||
appURL string
|
appURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
var containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
|
|
||||||
|
|
||||||
// NewHub creates a new Hub instance with default configuration
|
// NewHub creates a new Hub instance with default configuration
|
||||||
func NewHub(app core.App) *Hub {
|
func NewHub(app core.App) *Hub {
|
||||||
hub := &Hub{}
|
hub := &Hub{}
|
||||||
@@ -464,9 +461,6 @@ func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*syst
|
|||||||
if systemID == "" || containerID == "" {
|
if systemID == "" || containerID == "" {
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"})
|
return e.JSON(http.StatusBadRequest, map[string]string{"error": "system and container parameters are required"})
|
||||||
}
|
}
|
||||||
if !containerIDPattern.MatchString(containerID) {
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "invalid container parameter"})
|
|
||||||
}
|
|
||||||
|
|
||||||
system, err := h.sm.GetSystem(systemID)
|
system, err := h.sm.GetSystem(systemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "GET /containers/logs - with auth but invalid system should fail",
|
Name: "GET /containers/logs - with auth but invalid system should fail",
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
URL: "/api/beszel/containers/logs?system=invalid-system&container=0123456789ab",
|
URL: "/api/beszel/containers/logs?system=invalid-system&container=test-container",
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"Authorization": userToken,
|
"Authorization": userToken,
|
||||||
},
|
},
|
||||||
@@ -553,39 +553,6 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
|||||||
ExpectedContent: []string{"system not found"},
|
ExpectedContent: []string{"system not found"},
|
||||||
TestAppFactory: testAppFactory,
|
TestAppFactory: testAppFactory,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "GET /containers/logs - traversal container should fail validation",
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: "/api/beszel/containers/logs?system=" + system.Id + "&container=..%2F..%2Fversion",
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Authorization": userToken,
|
|
||||||
},
|
|
||||||
ExpectedStatus: 400,
|
|
||||||
ExpectedContent: []string{"invalid container parameter"},
|
|
||||||
TestAppFactory: testAppFactory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "GET /containers/info - traversal container should fail validation",
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: "/api/beszel/containers/info?system=" + system.Id + "&container=../../version?x=",
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Authorization": userToken,
|
|
||||||
},
|
|
||||||
ExpectedStatus: 400,
|
|
||||||
ExpectedContent: []string{"invalid container parameter"},
|
|
||||||
TestAppFactory: testAppFactory,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "GET /containers/info - non-hex container should fail validation",
|
|
||||||
Method: http.MethodGet,
|
|
||||||
URL: "/api/beszel/containers/info?system=" + system.Id + "&container=container_name",
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Authorization": userToken,
|
|
||||||
},
|
|
||||||
ExpectedStatus: 400,
|
|
||||||
ExpectedContent: []string{"invalid container parameter"},
|
|
||||||
TestAppFactory: testAppFactory,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Auth Optional Routes - Should work without authentication
|
// Auth Optional Routes - Should work without authentication
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -593,7 +593,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
if (showMax) {
|
if (showMax) {
|
||||||
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
|
return data?.stats?.bm?.[0] ?? (data?.stats?.nsm ?? 0) * 1024 * 1024
|
||||||
}
|
}
|
||||||
return data?.stats?.b?.[0] ?? (data?.stats?.ns ?? 0) * 1024 * 1024
|
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
|
||||||
},
|
},
|
||||||
color: 5,
|
color: 5,
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
@@ -604,7 +604,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
if (showMax) {
|
if (showMax) {
|
||||||
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
|
return data?.stats?.bm?.[1] ?? (data?.stats?.nrm ?? 0) * 1024 * 1024
|
||||||
}
|
}
|
||||||
return data?.stats?.b?.[1] ?? (data?.stats?.nr ?? 0) * 1024 * 1024
|
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
|
||||||
},
|
},
|
||||||
color: 2,
|
color: 2,
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
|
|||||||
@@ -2,27 +2,7 @@
|
|||||||
|
|
||||||
- Add outbound heartbeat monitoring to external services (BetterStack, Uptime Kuma, Healthchecks.io, etc.) with system status summary payload. Configured via `BESZEL_HUB_HEARTBEAT_URL`, `BESZEL_HUB_HEARTBEAT_INTERVAL`, and `BESZEL_HUB_HEARTBEAT_METHOD` environment variables.
|
- Add outbound heartbeat monitoring to external services (BetterStack, Uptime Kuma, Healthchecks.io, etc.) with system status summary payload. Configured via `BESZEL_HUB_HEARTBEAT_URL`, `BESZEL_HUB_HEARTBEAT_INTERVAL`, and `BESZEL_HUB_HEARTBEAT_METHOD` environment variables.
|
||||||
|
|
||||||
- Add GPU monitoring for Apple Silicon. (#1747, #1746)
|
- Add Heartbeat settings page to the admin UI with status display, configuration reference, and test button.
|
||||||
|
|
||||||
- Add `nvtop` integration for expanded GPU compatibility.
|
|
||||||
|
|
||||||
- Add `GPU_COLLECTOR` environment variable to manually specify the GPU collector(s).
|
|
||||||
|
|
||||||
- Add eMMC health monitoring via sysfs. (#1736)
|
|
||||||
|
|
||||||
- Add uptime to systems table. (#1719)
|
|
||||||
|
|
||||||
- Add `DISABLE_SSH` environment variable to disable SSH agent functionality. (#1061)
|
|
||||||
|
|
||||||
- Add `fingerprint` command to the agent. (#1726)
|
|
||||||
|
|
||||||
- Include GTT memory in AMD GPU metrics and improve device name lookup. (#1569)
|
|
||||||
|
|
||||||
- Fix issue where the agent could report incorrect root disk I/O when running in Docker. (#1737)
|
|
||||||
|
|
||||||
- Update Go to 1.26.
|
|
||||||
|
|
||||||
- Add `InstallMethod` parameter to Windows install script.
|
|
||||||
|
|
||||||
## 0.18.3
|
## 0.18.3
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user