ce7cb702218693d08ea6d83464d2457c9250027c

This commit is contained in:
ms2k 2026-11-12 20:46:28 +09:00
commit 34c5f8963c
23 changed files with 145336 additions and 0 deletions

2
packages/grafana-rcon/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/dist/
/node_modules/

View file

@ -0,0 +1,13 @@
.PHONY: build test clean
build:
npm run build
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o dist/rcon_linux_amd64 ./backend
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -o dist/rcon_linux_arm64 ./backend
test:
npm run check
go test -race ./backend
clean:
rm -rf dist

View file

@ -0,0 +1,66 @@
package main
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"time"
"github.com/grafana/authlib/authn"
"github.com/grafana/grafana-plugin-sdk-go/config"
)
type authorizer struct {
url string
client *http.Client
verifier authn.Verifier[struct{}]
}
func newAuthorizer(base string) *authorizer {
base = strings.TrimRight(base, "/")
client := &http.Client{Timeout: 10 * time.Second}
keys := authn.NewKeyRetriever(authn.KeyRetrieverConfig{SigningKeysURL: base + "/api/signing-keys/keys"}, authn.WithHTTPClientKeyRetrieverOpt(client))
return &authorizer{
url: base,
client: client,
verifier: authn.NewVerifier[struct{}](authn.VerifierConfig{}, authn.TokenTypeID, keys),
}
}
func (a *authorizer) canSubmit(req *http.Request) (bool, error) {
cfg := config.GrafanaConfigFromContext(req.Context())
token, err := cfg.PluginAppClientSecret()
if err != nil || token == "" {
return false, errors.New("plugin service account is unavailable")
}
claims, err := a.verifier.Verify(req.Context(), req.Header.Get("X-Grafana-Id"))
if err != nil {
return false, err
}
id, ok := strings.CutPrefix(claims.Subject, "user:")
if !ok || id == "" {
return false, nil
}
request, err := http.NewRequestWithContext(req.Context(), "GET", a.url+"/api/users/"+url.PathEscape(id), nil)
if err != nil {
return false, err
}
request.Header.Set("Authorization", "Bearer "+token)
response, err := a.client.Do(request)
if err != nil {
return false, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return false, errors.New("user lookup is unavailable")
}
var profile struct {
Admin bool `json:"isGrafanaAdmin"`
}
if err := json.NewDecoder(response.Body).Decode(&profile); err != nil {
return false, err
}
return profile.Admin, nil
}

View file

@ -0,0 +1,158 @@
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/grafana/grafana-plugin-sdk-go/config"
)
type authFixture struct {
key *ecdsa.PrivateKey
server *httptest.Server
admin atomic.Bool
status atomic.Int32
badJSON atomic.Bool
keyCalls atomic.Int32
userCalls atomic.Int32
bearer atomic.Value
}
func newAuthFixture(t *testing.T) *authFixture {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
f := &authFixture{key: key}
f.admin.Store(true)
f.status.Store(http.StatusOK)
f.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/signing-keys/keys":
f.keyCalls.Add(1)
_ = json.NewEncoder(w).Encode(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: &key.PublicKey, KeyID: "synthetic"}}})
case "/api/users/17":
f.userCalls.Add(1)
f.bearer.Store(req.Header.Get("Authorization"))
w.WriteHeader(int(f.status.Load()))
if f.badJSON.Load() {
_, _ = w.Write([]byte("invalid JSON"))
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"isGrafanaAdmin": f.admin.Load(), "role": "Admin"})
default:
http.NotFound(w, req)
}
}))
t.Cleanup(f.server.Close)
return f
}
func signedID(t *testing.T, key *ecdsa.PrivateKey, subject string, expires time.Time) string {
t.Helper()
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jose.JSONWebKey{Key: key, KeyID: "synthetic"}}, new(jose.SignerOptions).WithType("jwt"))
if err != nil {
t.Fatal(err)
}
token, err := jwt.Signed(signer).Claims(jwt.Claims{Subject: subject, Expiry: jwt.NewNumericDate(expires)}).Serialize()
if err != nil {
t.Fatal(err)
}
return token
}
func authRequest(identity, secret string) *http.Request {
ctx := config.WithGrafanaConfig(context.Background(), config.NewGrafanaCfg(map[string]string{config.AppClientSecret: secret}))
req := httptest.NewRequest("POST", "/command", strings.NewReader(`{"command":"list"}`)).WithContext(ctx)
req.Header.Set("X-Grafana-Id", identity)
req.Header.Set("X-Grafana-User", "forged-admin")
req.Header.Set("X-Grafana-Role", "Admin")
return req
}
func TestRoleRevocation(t *testing.T) {
f := newAuthFixture(t)
auth := newAuthorizer(f.server.URL)
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
calls := 0
c := &console{allowed: auth.canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
for _, step := range []struct {
admin bool
secret string
status int
}{
{true, "synthetic-first", 200},
{false, "synthetic-second", 403},
{false, "synthetic-second", 403},
{true, "synthetic-second", 200},
} {
f.admin.Store(step.admin)
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, authRequest(identity, step.secret))
if w.Code != step.status {
t.Fatalf("status=%d want=%d", w.Code, step.status)
}
}
if calls != 2 || f.keyCalls.Load() != 1 || f.userCalls.Load() != 4 || f.bearer.Load() != "Bearer synthetic-second" {
t.Fatalf("commands=%d keys=%d users=%d bearer=%v", calls, f.keyCalls.Load(), f.userCalls.Load(), f.bearer.Load())
}
}
func TestIdentityFailures(t *testing.T) {
for _, name := range []string{"missing ID", "forged ID", "expired ID", "wrong signature", "service account", "empty user", "missing secret", "user API failure", "invalid profile", "organization admin"} {
t.Run(name, func(t *testing.T) {
f := newAuthFixture(t)
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
secret := "synthetic-service"
status := http.StatusServiceUnavailable
switch name {
case "missing ID":
identity = ""
case "forged ID":
identity = "admin"
case "expired ID":
identity = signedID(t, f.key, "user:17", time.Now().Add(-time.Hour))
case "wrong signature":
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
identity = signedID(t, key, "user:17", time.Now().Add(time.Minute))
case "service account":
identity = signedID(t, f.key, "service-account:17", time.Now().Add(time.Minute))
status = http.StatusForbidden
case "empty user":
identity = signedID(t, f.key, "user:", time.Now().Add(time.Minute))
status = http.StatusForbidden
case "missing secret":
secret = ""
case "user API failure":
f.status.Store(http.StatusInternalServerError)
case "invalid profile":
f.badJSON.Store(true)
case "organization admin":
f.admin.Store(false)
status = http.StatusForbidden
}
calls := 0
c := &console{allowed: newAuthorizer(f.server.URL).canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, authRequest(identity, secret))
if w.Code != status || calls != 0 {
t.Fatalf("status=%d want=%d commands=%d", w.Code, status, calls)
}
})
}
}

View file

@ -0,0 +1,140 @@
package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"os"
"strings"
"github.com/gorcon/rcon"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/app"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
)
const pluginID = "quorate-rcon-app"
type serverConfig struct {
APIURL string `json:"apiUrl"`
Address string `json:"address"`
PasswordPath string `json:"passwordPath"`
}
type command struct {
Command string `json:"command"`
}
type console struct {
backend.CallResourceHandler
allowed func(*http.Request) (bool, error)
execute func(context.Context, string) (string, error)
check func(context.Context) error
}
func newConsole(ctx context.Context, _ backend.AppInstanceSettings) (instancemgmt.Instance, error) {
data, err := os.ReadFile("/etc/grafana/rcon/config.json")
if err != nil {
return nil, err
}
var cfg serverConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
if cfg.Address == "" || cfg.PasswordPath == "" || cfg.APIURL == "" {
return nil, errors.New("Grafana API URL, RCON address and password file are required")
}
auth := newAuthorizer(cfg.APIURL)
c := &console{allowed: auth.canSubmit, execute: func(ctx context.Context, cmd string) (string, error) {
password, err := cfg.password()
if err != nil {
return "", err
}
return execute(ctx, cfg.Address, password, cmd)
}, check: func(ctx context.Context) error {
password, err := cfg.password()
if err != nil {
return err
}
return checkRcon(ctx, cfg.Address, password)
}}
c.CallResourceHandler = httpadapter.New(c.routes())
return c, nil
}
func (cfg serverConfig) password() (string, error) {
data, err := os.ReadFile(cfg.PasswordPath)
return strings.TrimSuffix(string(data), "\n"), err
}
func (c *console) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /access", func(w http.ResponseWriter, req *http.Request) {
allowed, err := c.allowed(req)
if err != nil {
http.Error(w, "authorization unavailable", http.StatusServiceUnavailable)
return
}
respond(w, map[string]bool{"submit": allowed})
})
mux.HandleFunc("POST /command", c.submit)
mux.HandleFunc("GET /health", func(w http.ResponseWriter, req *http.Request) {
if !c.authorize(w, req) {
return
}
if err := c.check(req.Context()); err != nil {
http.Error(w, "RCON connection or authentication failed", http.StatusBadGateway)
return
}
respond(w, map[string]string{"status": "ok"})
})
return mux
}
func respond(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(data)
}
func (c *console) authorize(w http.ResponseWriter, req *http.Request) bool {
allowed, err := c.allowed(req)
if err != nil {
http.Error(w, "authorization unavailable", http.StatusServiceUnavailable)
return false
}
if !allowed {
http.Error(w, "permission denied", http.StatusForbidden)
return false
}
return true
}
func (c *console) submit(w http.ResponseWriter, req *http.Request) {
if !c.authorize(w, req) {
return
}
var input command
if err := json.NewDecoder(req.Body).Decode(&input); err != nil {
http.Error(w, "invalid command", http.StatusBadRequest)
return
}
if strings.TrimSpace(input.Command) == "" || len(input.Command) > rcon.MaxCommandLen || strings.ContainsAny(input.Command, "\x00\r\n") {
http.Error(w, "enter one command of 1–1000 bytes", http.StatusBadRequest)
return
}
output, err := c.execute(req.Context(), input.Command)
if err != nil {
http.Error(w, "RCON request failed; execution may have occurred. Check server state before resubmitting.", http.StatusBadGateway)
return
}
respond(w, map[string]string{"output": output})
}
func main() {
if err := app.Manage(pluginID, newConsole, app.ManageOpts{}); err != nil {
os.Exit(1)
}
}

View file

@ -0,0 +1,75 @@
package main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCommandBoundary(t *testing.T) {
for _, tc := range []struct {
name, method, body string
allow bool
authErr error
status, calls int
}{
{"viewer", "POST", `{"command":"stop"}`, false, nil, 403, 0},
{"auth failure", "POST", `{"command":"stop"}`, false, errors.New("offline"), 503, 0},
{"query refresh", "GET", `{"command":"stop"}`, true, nil, 405, 0},
{"empty", "POST", `{"command":" "}`, true, nil, 400, 0},
{"newline", "POST", `{"command":"list\nstop"}`, true, nil, 400, 0},
{"nul", "POST", `{"command":"list\u0000stop"}`, true, nil, 400, 0},
{"oversized", "POST", `{"command":"` + strings.Repeat("x", 1001) + `"}`, true, nil, 400, 0},
{"admin", "POST", `{"command":"list"}`, true, nil, 200, 1},
} {
t.Run(tc.name, func(t *testing.T) {
calls := 0
c := &console{allowed: func(*http.Request) (bool, error) { return tc.allow, tc.authErr }, execute: func(context.Context, string) (string, error) { calls++; return "Nobody online", nil }}
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, httptest.NewRequest(tc.method, "/command", strings.NewReader(tc.body)))
if w.Code != tc.status || calls != tc.calls {
t.Fatalf("status=%d calls=%d", w.Code, calls)
}
})
}
}
func TestNoRetry(t *testing.T) {
calls := 0
c := &console{allowed: func(*http.Request) (bool, error) { return true, nil }, execute: func(context.Context, string) (string, error) { calls++; return "", errors.New("lost response") }}
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, httptest.NewRequest("POST", "/command", strings.NewReader(`{"command":"say once"}`)))
if w.Code != 502 || calls != 1 {
t.Fatalf("status=%d calls=%d", w.Code, calls)
}
}
func TestHealth(t *testing.T) {
for _, tc := range []struct {
name string
allow bool
err error
status, calls int
}{
{"viewer", false, nil, 403, 0},
{"admin", true, nil, 200, 1},
{"unavailable RCON", true, errors.New("offline"), 502, 1},
} {
t.Run(tc.name, func(t *testing.T) {
checks, commands := 0, 0
c := &console{
allowed: func(*http.Request) (bool, error) { return tc.allow, nil },
check: func(context.Context) error { checks++; return tc.err },
execute: func(context.Context, string) (string, error) { commands++; return "", nil },
}
w := httptest.NewRecorder()
c.routes().ServeHTTP(w, httptest.NewRequest("GET", "/health", nil))
if w.Code != tc.status || checks != tc.calls || commands != 0 {
t.Fatalf("status=%d checks=%d commands=%d", w.Code, checks, commands)
}
})
}
}

View file

@ -0,0 +1,97 @@
package main
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/gorcon/rcon"
)
const (
authID = 0
cmdID = 1
endID = 2
)
func authenticate(conn net.Conn, password string) error {
if _, err := rcon.NewPacket(rcon.SERVERDATA_AUTH, authID, password).WriteTo(conn); err != nil {
return err
}
var packet rcon.Packet
if _, err := packet.ReadFrom(conn); err != nil {
return err
}
if packet.Type != rcon.SERVERDATA_AUTH_RESPONSE {
return rcon.ErrInvalidAuthResponse
}
if packet.ID == -1 {
return rcon.ErrAuthFailed
}
if packet.ID != authID {
return rcon.ErrInvalidPacketID
}
return nil
}
func withRcon(ctx context.Context, address, password string, action func(net.Conn) (string, error)) (string, error) {
dialer := net.Dialer{Timeout: rcon.DefaultDialTimeout}
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return "", err
}
defer conn.Close()
cancel := context.AfterFunc(ctx, func() { _ = conn.Close() })
defer cancel()
if err := conn.SetDeadline(time.Now().Add(rcon.DefaultDeadline)); err != nil {
return "", err
}
if err := authenticate(conn, password); err != nil {
return "", err
}
if err := conn.SetDeadline(time.Now().Add(rcon.DefaultDeadline)); err != nil {
return "", err
}
return action(conn)
}
func checkRcon(ctx context.Context, address, password string) error {
_, err := withRcon(ctx, address, password, func(net.Conn) (string, error) { return "", nil })
return err
}
func execute(ctx context.Context, address, password, command string) (string, error) {
return withRcon(ctx, address, password, func(conn net.Conn) (string, error) {
if _, err := rcon.NewPacket(rcon.SERVERDATA_EXECCOMMAND, cmdID, command).WriteTo(conn); err != nil {
return "", err
}
var first rcon.Packet
if _, err := first.ReadFrom(conn); err != nil {
return "", err
}
if first.ID != cmdID || first.Type != rcon.SERVERDATA_RESPONSE_VALUE {
return "", fmt.Errorf("unexpected RCON response")
}
// Wait for the command response before sending the delimiter; Minecraft drops pipelined requests.
if _, err := rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, endID, "").WriteTo(conn); err != nil {
return "", err
}
var output strings.Builder
output.WriteString(first.Body())
for {
var packet rcon.Packet
if _, err := packet.ReadFrom(conn); err != nil {
return "", err
}
if packet.ID == endID {
return output.String(), nil
}
if packet.ID != cmdID || packet.Type != rcon.SERVERDATA_RESPONSE_VALUE {
return "", fmt.Errorf("unexpected RCON response")
}
output.WriteString(packet.Body())
}
})
}

View file

@ -0,0 +1,128 @@
package main
import (
"bytes"
"context"
"errors"
"net"
"testing"
"time"
"github.com/gorcon/rcon"
)
func TestMultipart(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
done := make(chan error, 1)
go func() {
conn, err := listener.Accept()
if err != nil {
done <- err
return
}
defer conn.Close()
var packet rcon.Packet
if _, err := packet.ReadFrom(conn); err != nil {
done <- err
return
}
if _, err := rcon.NewPacket(2, packet.ID, "").WriteTo(conn); err != nil {
done <- err
return
}
if _, err := packet.ReadFrom(conn); err != nil {
done <- err
return
}
commandID := packet.ID
if _, err := rcon.NewPacket(0, commandID, "first packet\n").WriteTo(conn); err != nil {
done <- err
return
}
if _, err := packet.ReadFrom(conn); err != nil {
done <- err
return
}
markerID := packet.ID
for _, response := range []*rcon.Packet{
rcon.NewPacket(0, commandID, "second packet"),
rcon.NewPacket(0, markerID, "Unknown request 0"),
} {
if _, err := response.WriteTo(conn); err != nil {
done <- err
return
}
}
done <- nil
}()
output, err := execute(context.Background(), listener.Addr().String(), "synthetic", "help")
if err != nil {
t.Fatal(err)
}
if output != "first packet\nsecond packet" {
t.Fatalf("output=%q", output)
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestSplitAuth(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
_ = client.SetDeadline(time.Now().Add(time.Second))
done := make(chan error, 1)
go func() {
defer server.Close()
var request rcon.Packet
if _, err := request.ReadFrom(server); err != nil {
done <- err
return
}
var reply bytes.Buffer
_, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, request.ID, "").WriteTo(&reply)
packet := reply.Bytes()
for _, part := range [][]byte{packet[:len(packet)-1], packet[len(packet)-1:]} {
if _, err := server.Write(part); err != nil {
done <- err
return
}
}
_, err := rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, cmdID, "ok").WriteTo(server)
done <- err
}()
if err := authenticate(client, "synthetic-password"); err != nil {
t.Fatal(err)
}
var response rcon.Packet
if _, err := response.ReadFrom(client); err != nil {
t.Fatal(err)
}
if response.ID != cmdID || response.Body() != "ok" {
t.Fatalf("id=%d body=%q", response.ID, response.Body())
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestAuthFailure(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
_ = client.SetDeadline(time.Now().Add(time.Second))
go func() {
defer server.Close()
var request rcon.Packet
_, _ = request.ReadFrom(server)
_, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, -1, "").WriteTo(server)
}()
if err := authenticate(client, "wrong-password"); !errors.Is(err, rcon.ErrAuthFailed) {
t.Fatalf("error=%v", err)
}
}

View file

@ -0,0 +1,9 @@
import { mkdir, cp, copyFile } from 'node:fs/promises';
await mkdir('dist', { recursive: true });
await copyFile('plugin.json', 'dist/plugin.json');
for (const name of ['panel', 'datasource']) {
await mkdir(`dist/${name}`, { recursive: true });
await copyFile(`${name}.json`, `dist/${name}/plugin.json`);
await cp('img', `dist/${name}/img`, { recursive: true });
}
await cp('img', 'dist/img', { recursive: true });

View file

@ -0,0 +1,23 @@
{
"type": "datasource",
"name": "Minecraft Console",
"id": "quorate-datasource",
"info": {
"author": {
"name": "Quorate"
},
"description": "Minecraft remote console",
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"version": "0.1.0",
"updated": "2026-10-06"
},
"dependencies": {
"grafanaDependency": ">=12.4.1",
"plugins": []
},
"metrics": true,
"annotations": false
}

View file

@ -0,0 +1,84 @@
module quorate.dev/grafana-rcon
go 1.26.5
require (
github.com/go-jose/go-jose/v4 v4.1.5
github.com/gorcon/rcon v1.4.0
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe
github.com/grafana/grafana-plugin-sdk-go v0.296.5
)
require (
github.com/apache/arrow-go/v18 v18.8.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe // indirect
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e // indirect
github.com/grafana/otel-profiling-go v0.6.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.12 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/jaegertracing/jaeger-idl v0.12.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.3.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.29 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/procfs v0.22.0 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3 // indirect
go.opentelemetry.io/otel v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/otel/sdk v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd // indirect
google.golang.org/grpc v1.84.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)

View file

@ -0,0 +1,248 @@
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apache/arrow-go/v18 v18.8.0 h1:BLOzbPv7bxMPgXPacAg6HQjnxupYsZzC4tf+FkqPU/M=
github.com/apache/arrow-go/v18 v18.8.0/go.mod h1:uJCFfCwq0KsxCmsCfQg4ft+LsW+iHYzAXiSDh5ug/8U=
github.com/apache/thrift v0.24.0 h1:zy31L1a49QTNB2bG1BBfMXol3yJrTH975G3pPubQVLQ=
github.com/apache/thrift v0.24.0/go.mod h1:zPt6WxgvTOM6hF92y8C+MkEM5LMxZuk4JcQOiU4Esvs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20260912003405-686a5c723acc h1:DGGf6Tq9Qz1g0kbDvYCzoqRUpNRw8bicEHH+USB7ROE=
github.com/chromedp/cdproto v0.0.0-20260912003405-686a5c723acc/go.mod h1:3v4FIp5njIUyPDvqXsxEOxnB34lijG0up98/5kM1KaE=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.9.1 h1:1gQuANpCxN2UQKWwkNPpWqAk1LnAMYuBxqfIdU8mMoE=
github.com/elazarl/goproxy v1.9.1/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe h1:R0tVnQLt1m4Qn0F2tw7G6JUGVTZXg8x0ZcYv2zs5PXs=
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe/go.mod h1:1y/QFuWzHw/S70DfBTA9odWcmqd/r/YuCUylNXohIrI=
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe h1:rZ4YTid1IkjrwEMT5zilAfZrt8wzM7k1C4iiFaOEOWc=
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe/go.mod h1:/nf5Y9wbsrPAOGKScnfFYO9Bdb7eLsP/VPEuJUsDzzM=
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e h1:zP30ZbtnWjy0KJxToqCLARauAtgFD+TAEB/sk4IBb9s=
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e/go.mod h1:Tl9i4F2CPp3cOiXo06JObTh05tENG/NV/ombn2XOY7Y=
github.com/grafana/grafana-plugin-sdk-go v0.296.5 h1:86zOFJV6mSSYMPvcug6X4TlrrPm77q+PQH2VYGVV5ME=
github.com/grafana/grafana-plugin-sdk-go v0.296.5/go.mod h1:E2WBpInzUESFrzNow91khsHYJPVEKTV0O05XLDMMKus=
github.com/grafana/otel-profiling-go v0.6.0 h1:W7lOZaJj4IJISXMcM1UBk3fJF3tzF2OD6MJBJaQp1H8=
github.com/grafana/otel-profiling-go v0.6.0/go.mod h1:cqLIDgNXlnzknJ0WLiEe+JPjZk2MZ4ftMdqRJRWj1ZM=
github.com/grafana/pyroscope-go/godeltaprof v0.1.12 h1:X6OemT2WcLtxdmNukEQuIp0c+efWVI/tBTd0oeWeDHI=
github.com/grafana/pyroscope-go/godeltaprof v0.1.12/go.mod h1:aNSXN1bn1VHAd06EiepmwhAabHsMc67gx8itecdF2c8=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 h1:9ZJYjPEJpcleIKcqysXOo14NLQYwu23fzmKRFpIjPjc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4/go.mod h1:0xydIeg2omQ9DH6zYMP24Kk57793rnLUvP8fwdbFvz8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs=
github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jaegertracing/jaeger-idl v0.12.0 h1:FDk3ezIqKk7n9/gzxG9NNjDWzZZLh0GiCxhF4H1LRNU=
github.com/jaegertracing/jaeger-idl v0.12.0/go.mod h1:wWzFftH47XtPRkOM25NPNZ7zBhREWB5HtZBsWj25eW0=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg=
github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0 h1:oFNJW32h2SXnET7XXstgT7pVh4vN+jW+GfiIaBguIZE=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0/go.mod h1:+H3sPOFwag14eMHTPMElZtV0e4YfVZ/85KgrKUCB5FI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0 h1:uxl0SGcmuBkHj/Adl9oftEAyiawQBPL5RzMAmt/Yvq4=
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0/go.mod h1:LiOkxCIvoLofmRps7f8l0NkBtmObnAyQ5trteFs6wj8=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3 h1:20rKrm6q8YlSVwVObflwKd2abo9f8b3WsxJ6YTyQtE8=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3/go.mod h1:2yFxWbgN2VupcMTWu57donCZ9I1lALhawQjNAK7Xmm4=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd h1:jgCQeWccunvocKNK2RjhQHNiSjSeMMuBpQ/5pbP+2lA=
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:r4KD2hOq82JBWpTWkJ9NZLf6EwmRvAPIFmk7hPNtd+0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd h1:29LZPNIJg+GEmJwZXruJJRwqy/wnWAYZXDtROToV/aQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect x="2" y="2" width="60" height="60" rx="8" fill="#2e7d32"/><path d="M15 21l12 11-12 11m18 0h16" stroke="white" stroke-width="5" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 236 B

6580
packages/grafana-rcon/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
{
"name": "grafana-rcon",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc --outDir build && rollup -c && node build.mjs",
"check": "tsc --noEmit"
},
"dependencies": {
"@grafana/data": "12.4.1",
"@grafana/runtime": "12.4.1",
"@grafana/ui": "12.4.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "7.8.2"
},
"devDependencies": {
"rollup": "4.67.3",
"typescript": "7.0.2",
"tslib": "2.8.1",
"@types/react": "18.3.27"
}
}

View file

@ -0,0 +1,21 @@
{
"type": "panel",
"name": "Minecraft Console",
"id": "quorate-rcon-panel",
"info": {
"author": {
"name": "Quorate"
},
"description": "Minecraft remote console",
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"version": "0.1.0",
"updated": "2026-10-06"
},
"dependencies": {
"grafanaDependency": ">=12.4.1",
"plugins": []
}
}

View file

@ -0,0 +1,32 @@
{
"type": "app",
"name": "Minecraft Console",
"id": "quorate-rcon-app",
"backend": true,
"executable": "rcon",
"autoEnabled": true,
"info": {
"author": {
"name": "Quorate"
},
"description": "Minecraft remote console",
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"version": "0.1.0",
"updated": "2026-10-06"
},
"dependencies": {
"grafanaDependency": ">=12.4.1",
"plugins": []
},
"iam": {
"permissions": [
{
"action": "users:read",
"scope": "global.users:*"
}
]
}
}

View file

@ -0,0 +1,5 @@
export default ['app', 'panel', 'datasource'].map(name => ({
input: `build/${name}.js`,
output: { file: `dist/${name === 'app' ? '' : name + '/'}module.js`, format: 'system' },
external: ['react', '@grafana/data', '@grafana/runtime', '@grafana/ui', 'rxjs', 'react/jsx-runtime'],
}));

View file

@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Alert, Button, Input, Stack } from '@grafana/ui';
const root = '/api/plugins/quorate-rcon-app/resources';
export function Console() {
const [allowed, setAllowed] = useState(false);
const [command, setCommand] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [transcript, setTranscript] = useState<Array<{ command: string; output: string }>>([]);
useEffect(() => {
getBackendSrv().get<{ submit: boolean }>(`${root}/access`).then(v => setAllowed(v.submit)).catch(() => setError('Command authorization is unavailable.'));
}, []);
async function submit(e: React.FormEvent) {
e.preventDefault();
if (busy || !allowed || !command.trim()) { return; }
setBusy(true); setError('');
const submitted = command;
try {
const result = await getBackendSrv().post<{ output: string }>(`${root}/command`, { command: submitted }, { retry: 0 });
setTranscript(items => [...items, { command: submitted, output: result.output }]);
setCommand('');
} catch (e) {
const failure = e as { data?: { message?: string }; message?: string };
setError(failure.data?.message || failure.message || 'Command failed. Check server state before resubmitting.');
} finally { setBusy(false); }
}
return <div style={{ height: '100%', overflow: 'auto', padding: 8 }}>
{!allowed && <Alert title="Commands require a Grafana administrator" severity="info">You can continue viewing server metrics.</Alert>}
{error && <Alert title={error} severity="error" />}
{allowed && <form onSubmit={submit}><Stack gap={1}>
<Input aria-label="Minecraft command" placeholder="list" value={command} disabled={busy} onChange={e => setCommand(e.currentTarget.value)} />
<Button type="submit" disabled={busy || !command.trim()}>{busy ? 'Submitting…' : 'Submit'}</Button>
</Stack></form>}
<div role="log" aria-label="Command output">
{transcript.map((item, i) => <div key={i}><strong>{`> ${item.command}`}</strong><pre style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>{item.output}</pre></div>)}
</div>
</div>;
}

View file

@ -0,0 +1,2 @@
import { AppPlugin } from '@grafana/data';
export const plugin = new AppPlugin();

Some files were not shown because too many files have changed in this diff Show more