feat: implement Phase 2 (Go Agent) and Phase 3 (React Frontend MVP)
Phase 2 - Go Agent Core: - gRPC client with exponential backoff reconnect logic - Command executor (PowerShell/sh cross-platform) - Proto stubs regenerated with module= option (correct output path) - gRPC upgraded to v1.79.3 (BidiStreamingClient support) Phase 3 - React Frontend MVP: - Vite + React 18 + TypeScript setup with Tailwind CSS v4 - TanStack Query for data fetching, API client + TypeScript types - Dashboard page: stats cards (agents/status/tickets) + sortable agents table - Agent detail page: CPU/RAM charts (Recharts), disk usage, shell command executor - Tickets page: CRUD with modals, filters, sortable table - Dark mode with CSS custom properties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
83
Agent/internal/connection/grpc_client.go
Normal file
83
Agent/internal/connection/grpc_client.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pb "nexusrmm.local/agent/pkg/proto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
type GrpcClient struct {
|
||||
conn *grpc.ClientConn
|
||||
Client pb.AgentServiceClient
|
||||
address string
|
||||
}
|
||||
|
||||
func NewGrpcClient(address string) (*GrpcClient, error) {
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 30 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(address, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GrpcClient{
|
||||
conn: conn,
|
||||
Client: pb.NewAgentServiceClient(conn),
|
||||
address: address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *GrpcClient) Close() {
|
||||
if g.conn != nil {
|
||||
g.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectWithRetry(address string, maxRetries int) (*GrpcClient, error) {
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
client, err := NewGrpcClient(address)
|
||||
if err != nil {
|
||||
log.Printf("Client creation %d/%d failed: %v", i+1, maxRetries, err)
|
||||
time.Sleep(backoffDuration(i))
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
client.conn.Connect()
|
||||
state := client.conn.GetState()
|
||||
client.conn.WaitForStateChange(ctx, state)
|
||||
cancel()
|
||||
|
||||
newState := client.conn.GetState()
|
||||
if newState == connectivity.Ready || newState == connectivity.Idle {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
log.Printf("Connection attempt %d/%d: state=%v", i+1, maxRetries, newState)
|
||||
client.Close()
|
||||
time.Sleep(backoffDuration(i))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to connect after %d attempts", maxRetries)
|
||||
}
|
||||
|
||||
func backoffDuration(attempt int) time.Duration {
|
||||
secs := 2 << attempt
|
||||
if secs > 30 {
|
||||
secs = 30
|
||||
}
|
||||
return time.Duration(secs) * time.Second
|
||||
}
|
||||
49
Agent/internal/executor/executor.go
Normal file
49
Agent/internal/executor/executor.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
Success bool
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context, command string, timeoutSec int) *Result {
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 300
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-Command", command)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "/bin/sh", "-c", command)
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
result := &Result{
|
||||
Stdout: stdout.String(),
|
||||
Stderr: stderr.String(),
|
||||
Success: err == nil,
|
||||
}
|
||||
if cmd.ProcessState != nil {
|
||||
result.ExitCode = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
if err != nil && result.ExitCode == 0 {
|
||||
result.ExitCode = -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user