Compare commits
5 Commits
e60b9f62f1
...
main
Author | SHA1 | Date | |
---|---|---|---|
4a79011793 | |||
0b62ac9ecd | |||
c9b14730ad | |||
2ee8a95a90 | |||
8bc1759dcb |
@@ -11,7 +11,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/mycelium_client"
|
"git.threefold.info/herocode/heroagent/pkg/mycelium_client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/mycelium_client"
|
"git.threefold.info/herocode/heroagent/pkg/mycelium_client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/sal/executor"
|
"git.threefold.info/herocode/heroagent/pkg/sal/executor"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
@@ -7,9 +7,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -12,16 +12,16 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroagent/api"
|
"git.threefold.info/herocode/heroagent/pkg/heroagent/api"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroagent/handlers"
|
"git.threefold.info/herocode/heroagent/pkg/heroagent/handlers"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroagent/pages"
|
"git.threefold.info/herocode/heroagent/pkg/heroagent/pages"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/sal/executor"
|
"git.threefold.info/herocode/heroagent/pkg/sal/executor"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/redisserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/redisserver"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
|
|
||||||
// "git.ourworld.tf/herocode/heroagent/pkg/vfs/interfaces"
|
// "git.threefold.info/herocode/heroagent/pkg/vfs/interfaces"
|
||||||
// "git.ourworld.tf/herocode/heroagent/pkg/vfs/interfaces/mock"
|
// "git.threefold.info/herocode/heroagent/pkg/vfs/interfaces/mock"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strconv" // Added strconv for JobID parsing
|
"strconv" // Added strconv for JobID parsing
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/herojobs"
|
"git.threefold.info/herocode/heroagent/pkg/herojobs"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/herojobs"
|
"git.threefold.info/herocode/heroagent/pkg/herojobs"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/logger"
|
"git.threefold.info/herocode/heroagent/pkg/logger"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
)
|
)
|
||||||
|
@@ -7,8 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroagent/handlers"
|
"git.threefold.info/herocode/heroagent/pkg/heroagent/handlers"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
)
|
)
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/herojobs"
|
"git.threefold.info/herocode/heroagent/pkg/herojobs"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessDisplayInfo represents information about a process for display purposes
|
// ProcessDisplayInfo represents information about a process for display purposes
|
||||||
|
@@ -35,7 +35,7 @@ Key features:
|
|||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new playbook from HeroScript text
|
// Create a new playbook from HeroScript text
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/herohandler"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory/herohandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -3,8 +3,8 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlers"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExampleHandler handles example actions
|
// ExampleHandler handles example actions
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/cmd/herohandler/internal"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/cmd/herohandler/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/herohandler"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory/herohandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
const exampleScript = `
|
const exampleScript = `
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runTutorial runs an interactive tutorial demonstrating the VM handler
|
// runTutorial runs an interactive tutorial demonstrating the VM handler
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VMHandler handles VM-related actions
|
// VMHandler handles VM-related actions
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
"git.threefold.info/herocode/heroagent/pkg/handlerfactory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The tutorial functions are defined in tutorial.go
|
// The tutorial functions are defined in tutorial.go
|
||||||
|
@@ -5,8 +5,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler interface defines methods that all handlers must implement
|
// Handler interface defines methods that all handlers must implement
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandlerFactory manages a collection of handlers
|
// HandlerFactory manages a collection of handlers
|
||||||
|
@@ -12,7 +12,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ANSI color codes for terminal output
|
// ANSI color codes for terminal output
|
||||||
|
@@ -3,7 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthHandler handles authentication actions
|
// AuthHandler handles authentication actions
|
||||||
|
@@ -5,9 +5,9 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BaseHandler provides common functionality for all handlers
|
// BaseHandler provides common functionality for all handlers
|
||||||
|
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandlerFactory manages a collection of handlers for processing HeroScript commands
|
// HandlerFactory manages a collection of handlers for processing HeroScript commands
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package herohandler
|
package herohandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFactory returns the handler factory
|
// GetFactory returns the handler factory
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/herohandler"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/herohandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -4,10 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
|
|
||||||
// "git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/heroscript/handlerfactory/fakehandler"
|
// "git.threefold.info/herocode/heroagent/pkg/handlerfactory/heroscript/handlerfactory/fakehandler"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/processmanagerhandler"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/processmanagerhandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeroHandler is the main handler factory that manages all registered handlers
|
// HeroHandler is the main handler factory that manages all registered handlers
|
||||||
|
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/playbook"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -3,8 +3,8 @@ package processmanagerhandler
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/handlerfactory/core"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
|
"git.threefold.info/herocode/heroagent/pkg/processmanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessManagerHandler handles process manager-related actions
|
// ProcessManagerHandler handles process manager-related actions
|
||||||
|
@@ -19,7 +19,7 @@ A Go package for parsing and manipulating parameters from text in a key-value fo
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/paramsparser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new parser
|
// Create a new parser
|
||||||
|
@@ -4,7 +4,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParamsParser represents a parameter parser that can handle various parameter sources
|
// ParamsParser represents a parameter parser that can handle various parameter sources
|
||||||
|
@@ -3,8 +3,8 @@ package playbook
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State represents the parser state
|
// State represents the parser state
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
"git.threefold.info/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActionType represents the type of action
|
// ActionType represents the type of action
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/heroservices/billing/models"
|
"git.threefold.info/herocode/heroagent/pkg/heroservices/billing/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -7,9 +7,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/data/ourdb"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/radixtree"
|
"git.threefold.info/herocode/heroagent/pkg/data/radixtree"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DBStore represents the central database store for all models
|
// DBStore represents the central database store for all models
|
||||||
|
@@ -9,7 +9,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
openaiproxy "git.ourworld.tf/herocode/heroagent/pkg/heroservices/openaiproxy"
|
openaiproxy "git.threefold.info/herocode/heroagent/pkg/heroservices/openaiproxy"
|
||||||
"github.com/openai/openai-go/option"
|
"github.com/openai/openai-go/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/jobsmanager"
|
"git.threefold.info/herocode/heroagent/pkg/jobsmanager"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
)
|
)
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/heroagent"
|
"git.threefold.info/herocode/heroagent/pkg/servers/heroagent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -20,6 +20,7 @@ func main() {
|
|||||||
enableRedisFlag := flag.Bool("redis", true, "Enable Redis server")
|
enableRedisFlag := flag.Bool("redis", true, "Enable Redis server")
|
||||||
enableWebDAVFlag := flag.Bool("webdav", true, "Enable WebDAV server")
|
enableWebDAVFlag := flag.Bool("webdav", true, "Enable WebDAV server")
|
||||||
enableUIFlag := flag.Bool("ui", true, "Enable UI server")
|
enableUIFlag := flag.Bool("ui", true, "Enable UI server")
|
||||||
|
enableJobsFlag := flag.Bool("jobs", true, "Enable Job Manager")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ func main() {
|
|||||||
config.EnableRedis = *enableRedisFlag
|
config.EnableRedis = *enableRedisFlag
|
||||||
config.EnableWebDAV = *enableWebDAVFlag
|
config.EnableWebDAV = *enableWebDAVFlag
|
||||||
config.EnableUI = *enableUIFlag
|
config.EnableUI = *enableUIFlag
|
||||||
|
config.EnableJobs = *enableJobsFlag
|
||||||
|
|
||||||
// Override with environment variables if provided
|
// Override with environment variables if provided
|
||||||
if redisPortStr := os.Getenv("REDIS_PORT"); redisPortStr != "" {
|
if redisPortStr := os.Getenv("REDIS_PORT"); redisPortStr != "" {
|
||||||
@@ -70,6 +72,9 @@ func main() {
|
|||||||
if config.EnableUI {
|
if config.EnableUI {
|
||||||
fmt.Printf("- UI server running on port %s\n", config.UI.Port)
|
fmt.Printf("- UI server running on port %s\n", config.UI.Port)
|
||||||
}
|
}
|
||||||
|
if config.EnableJobs {
|
||||||
|
fmt.Printf("- Job Manager running\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the main goroutine running
|
// Keep the main goroutine running
|
||||||
select {}
|
select {}
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/heroagent"
|
"git.threefold.info/herocode/heroagent/pkg/servers/heroagent"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
86
cmd/orpctest/main.go
Normal file
86
cmd/orpctest/main.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/openrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Parse command line flags
|
||||||
|
var (
|
||||||
|
specDir = flag.String("dir", "pkg/openrpc/services", "Directory containing OpenRPC specifications")
|
||||||
|
specName = flag.String("spec", "", "Name of the specification to display (optional)")
|
||||||
|
methodName = flag.String("method", "", "Name of the method to display (optional)")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Create a new OpenRPC Manager
|
||||||
|
manager := openrpc.NewORPCManager()
|
||||||
|
|
||||||
|
// Ensure the specification directory exists
|
||||||
|
if _, err := os.Stat(*specDir); os.IsNotExist(err) {
|
||||||
|
log.Fatalf("Specification directory does not exist: %s", *specDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all specifications from the directory
|
||||||
|
log.Printf("Loading specifications from %s...", *specDir)
|
||||||
|
if err := manager.LoadSpecs(*specDir); err != nil {
|
||||||
|
log.Fatalf("Failed to load specifications: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all loaded specifications
|
||||||
|
specs := manager.ListSpecs()
|
||||||
|
if len(specs) == 0 {
|
||||||
|
log.Fatalf("No specifications found in %s", *specDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Loaded specifications:")
|
||||||
|
for _, spec := range specs {
|
||||||
|
fmt.Printf("- %s\n", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a specification name is provided, display its methods
|
||||||
|
if *specName != "" {
|
||||||
|
spec := manager.GetSpec(*specName)
|
||||||
|
if spec == nil {
|
||||||
|
log.Fatalf("Specification not found: %s", *specName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nMethods in %s specification:\n", *specName)
|
||||||
|
methods := manager.ListMethods(*specName)
|
||||||
|
for _, method := range methods {
|
||||||
|
fmt.Printf("- %s\n", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a method name is provided, display its details
|
||||||
|
if *methodName != "" {
|
||||||
|
method := manager.GetMethod(*specName, *methodName)
|
||||||
|
if method == nil {
|
||||||
|
log.Fatalf("Method not found: %s", *methodName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nDetails for method '%s':\n", *methodName)
|
||||||
|
fmt.Printf("Description: %s\n", method.Description)
|
||||||
|
fmt.Printf("Parameters: %d\n", len(method.Params))
|
||||||
|
|
||||||
|
if len(method.Params) > 0 {
|
||||||
|
fmt.Println("Parameter list:")
|
||||||
|
for _, param := range method.Params {
|
||||||
|
required := ""
|
||||||
|
if param.Required {
|
||||||
|
required = " (required)"
|
||||||
|
}
|
||||||
|
fmt.Printf(" - %s%s: %s\n", param.Name, required, param.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Result: %s\n", method.Result.Name)
|
||||||
|
fmt.Printf("Examples: %d\n", len(method.Examples))
|
||||||
|
fmt.Printf("Errors: %d\n", len(method.Errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module git.ourworld.tf/herocode/heroagent
|
module git.threefold.info/herocode/heroagent
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
|
239
openrpc_manager_plan.md
Normal file
239
openrpc_manager_plan.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# OpenRPC Manager Implementation Plan
|
||||||
|
|
||||||
|
## 1. Understanding the Requirements
|
||||||
|
|
||||||
|
The task requires us to:
|
||||||
|
- Create an OpenRPC Manager (ORPCManager)
|
||||||
|
- Read JSON files from services in pkg/openrpc
|
||||||
|
- Create a model for OpenRPC spec in a separate file
|
||||||
|
- Read the OpenRPC specs into the model
|
||||||
|
- Keep these models in memory in the ORPCManager
|
||||||
|
- Create supporting methods like list_methods
|
||||||
|
- Create a command in @cmd/ to test this behavior
|
||||||
|
|
||||||
|
## 2. Project Structure
|
||||||
|
|
||||||
|
Here's the proposed file structure for our implementation:
|
||||||
|
|
||||||
|
```
|
||||||
|
pkg/
|
||||||
|
openrpc/
|
||||||
|
models/
|
||||||
|
spec.go # OpenRPC specification model
|
||||||
|
manager.go # ORPCManager implementation
|
||||||
|
cmd/
|
||||||
|
orpctest/
|
||||||
|
main.go # Test command for the ORPCManager
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Implementation Details
|
||||||
|
|
||||||
|
### 3.1 OpenRPC Specification Model (pkg/openrpc/models/spec.go)
|
||||||
|
|
||||||
|
We'll create a Go struct model that represents the OpenRPC specification based on the structure observed in zinit.json:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class OpenRPCSpec {
|
||||||
|
+string OpenRPC
|
||||||
|
+InfoObject Info
|
||||||
|
+Server[] Servers
|
||||||
|
+Method[] Methods
|
||||||
|
}
|
||||||
|
|
||||||
|
class InfoObject {
|
||||||
|
+string Version
|
||||||
|
+string Title
|
||||||
|
+string Description
|
||||||
|
+LicenseObject License
|
||||||
|
}
|
||||||
|
|
||||||
|
class LicenseObject {
|
||||||
|
+string Name
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
+string Name
|
||||||
|
+string URL
|
||||||
|
}
|
||||||
|
|
||||||
|
class Method {
|
||||||
|
+string Name
|
||||||
|
+string Description
|
||||||
|
+Parameter[] Params
|
||||||
|
+ResultObject Result
|
||||||
|
+Example[] Examples
|
||||||
|
+ErrorObject[] Errors
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parameter {
|
||||||
|
+string Name
|
||||||
|
+string Description
|
||||||
|
+bool Required
|
||||||
|
+SchemaObject Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResultObject {
|
||||||
|
+string Name
|
||||||
|
+string Description
|
||||||
|
+SchemaObject Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemaObject {
|
||||||
|
+string Type
|
||||||
|
+map[string]interface{} Properties
|
||||||
|
+SchemaObject Items
|
||||||
|
+map[string]SchemaObject AdditionalProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
class Example {
|
||||||
|
+string Name
|
||||||
|
+map[string]interface{}[] Params
|
||||||
|
+ExampleResultObject Result
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExampleResultObject {
|
||||||
|
+string Name
|
||||||
|
+interface{} Value
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorObject {
|
||||||
|
+int Code
|
||||||
|
+string Message
|
||||||
|
+string Data
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenRPCSpec --> InfoObject
|
||||||
|
OpenRPCSpec --> Server
|
||||||
|
OpenRPCSpec --> Method
|
||||||
|
Method --> Parameter
|
||||||
|
Method --> ResultObject
|
||||||
|
Method --> Example
|
||||||
|
Method --> ErrorObject
|
||||||
|
Parameter --> SchemaObject
|
||||||
|
ResultObject --> SchemaObject
|
||||||
|
Example --> ExampleResultObject
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 OpenRPC Manager (pkg/openrpc/manager.go)
|
||||||
|
|
||||||
|
The ORPCManager will be responsible for:
|
||||||
|
- Loading OpenRPC specifications from JSON files
|
||||||
|
- Storing and managing these specifications in memory
|
||||||
|
- Providing methods to access and manipulate the specifications
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class ORPCManager {
|
||||||
|
-map[string]*OpenRPCSpec specs
|
||||||
|
+NewORPCManager() *ORPCManager
|
||||||
|
+LoadSpecs(dir string) error
|
||||||
|
+LoadSpec(path string) error
|
||||||
|
+GetSpec(name string) *OpenRPCSpec
|
||||||
|
+ListSpecs() []string
|
||||||
|
+ListMethods(specName string) []string
|
||||||
|
+GetMethod(specName string, methodName string) *Method
|
||||||
|
}
|
||||||
|
|
||||||
|
ORPCManager --> OpenRPCSpec
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Test Command (cmd/orpctest/main.go)
|
||||||
|
|
||||||
|
We'll create a command-line tool to test the ORPCManager functionality:
|
||||||
|
- Initialize the ORPCManager
|
||||||
|
- Load specifications from the pkg/openrpc/services directory
|
||||||
|
- List available specifications
|
||||||
|
- List methods for each specification
|
||||||
|
- Display details for specific methods
|
||||||
|
|
||||||
|
## 4. Implementation Steps
|
||||||
|
|
||||||
|
1. **Create the OpenRPC Specification Model**:
|
||||||
|
- Define the Go structs for the OpenRPC specification
|
||||||
|
- Implement JSON marshaling/unmarshaling
|
||||||
|
- Add validation functions
|
||||||
|
|
||||||
|
2. **Implement the ORPCManager**:
|
||||||
|
- Create the manager struct with a map to store specifications
|
||||||
|
- Implement methods to load specifications from files
|
||||||
|
- Implement methods to access and manipulate specifications
|
||||||
|
|
||||||
|
3. **Create the Test Command**:
|
||||||
|
- Implement a command-line interface to test the ORPCManager
|
||||||
|
- Add options to list specifications, methods, and display details
|
||||||
|
|
||||||
|
4. **Write Tests**:
|
||||||
|
- Write unit tests for the OpenRPC model
|
||||||
|
- Write unit tests for the ORPCManager
|
||||||
|
- Write integration tests for the entire system
|
||||||
|
|
||||||
|
## 5. Detailed Method Specifications
|
||||||
|
|
||||||
|
### 5.1 ORPCManager Methods
|
||||||
|
|
||||||
|
#### NewORPCManager()
|
||||||
|
- Creates a new instance of the ORPCManager
|
||||||
|
- Initializes the specs map
|
||||||
|
|
||||||
|
#### LoadSpecs(dir string) error
|
||||||
|
- Reads all JSON files in the specified directory
|
||||||
|
- For each file, calls LoadSpec()
|
||||||
|
- Returns an error if any file fails to load
|
||||||
|
|
||||||
|
#### LoadSpec(path string) error
|
||||||
|
- Reads the JSON file at the specified path
|
||||||
|
- Parses the JSON into an OpenRPCSpec struct
|
||||||
|
- Validates the specification
|
||||||
|
- Stores the specification in the specs map using the filename (without extension) as the key
|
||||||
|
- Returns an error if any step fails
|
||||||
|
|
||||||
|
#### GetSpec(name string) *OpenRPCSpec
|
||||||
|
- Returns the OpenRPCSpec with the specified name
|
||||||
|
- Returns nil if the specification doesn't exist
|
||||||
|
|
||||||
|
#### ListSpecs() []string
|
||||||
|
- Returns a list of all loaded specification names
|
||||||
|
|
||||||
|
#### ListMethods(specName string) []string
|
||||||
|
- Returns a list of all method names in the specified specification
|
||||||
|
- Returns an empty list if the specification doesn't exist
|
||||||
|
|
||||||
|
#### GetMethod(specName string, methodName string) *Method
|
||||||
|
- Returns the Method with the specified name from the specified specification
|
||||||
|
- Returns nil if the specification or method doesn't exist
|
||||||
|
|
||||||
|
## 6. Example Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Initialize the ORPCManager
|
||||||
|
manager := openrpc.NewORPCManager()
|
||||||
|
|
||||||
|
// Load all specifications from the services directory
|
||||||
|
err := manager.LoadSpecs("pkg/openrpc/services")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load specifications: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all loaded specifications
|
||||||
|
specs := manager.ListSpecs()
|
||||||
|
fmt.Println("Loaded specifications:")
|
||||||
|
for _, spec := range specs {
|
||||||
|
fmt.Printf("- %s\n", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all methods in the zinit specification
|
||||||
|
methods := manager.ListMethods("zinit")
|
||||||
|
fmt.Println("\nMethods in zinit specification:")
|
||||||
|
for _, method := range methods {
|
||||||
|
fmt.Printf("- %s\n", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get details for a specific method
|
||||||
|
method := manager.GetMethod("zinit", "service_list")
|
||||||
|
if method != nil {
|
||||||
|
fmt.Printf("\nDetails for method 'service_list':\n")
|
||||||
|
fmt.Printf("Description: %s\n", method.Description)
|
||||||
|
fmt.Printf("Parameters: %d\n", len(method.Params))
|
||||||
|
fmt.Printf("Examples: %d\n", len(method.Examples))
|
||||||
|
}
|
@@ -14,7 +14,7 @@ Dedupestor is a Go package that provides a key-value store with deduplication ba
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/dedupestor"
|
"git.threefold.info/herocode/heroagent/pkg/dedupestor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new dedupe store
|
// Create a new dedupe store
|
||||||
|
@@ -7,8 +7,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/data/ourdb"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/radixtree"
|
"git.threefold.info/herocode/heroagent/pkg/data/radixtree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MaxValueSize is the maximum allowed size for values (1MB)
|
// MaxValueSize is the maximum allowed size for values (1MB)
|
||||||
|
@@ -18,7 +18,7 @@ The DocTree package provides functionality for managing collections of markdown
|
|||||||
### Creating a DocTree
|
### Creating a DocTree
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.ourworld.tf/herocode/heroagent/pkg/doctree"
|
import "git.threefold.info/herocode/heroagent/pkg/doctree"
|
||||||
|
|
||||||
// Create a new DocTree with a path and name
|
// Create a new DocTree with a path and name
|
||||||
dt, err := doctree.New("/path/to/collection", "My Collection")
|
dt, err := doctree.New("/path/to/collection", "My Collection")
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collection represents a collection of markdown pages and files
|
// Collection represents a collection of markdown pages and files
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global variable to track the current DocTree instance
|
// Global variable to track the current DocTree instance
|
||||||
|
@@ -34,7 +34,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/ourdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -78,7 +78,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/ourdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -4,7 +4,7 @@ package radixtree
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/data/ourdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node represents a node in the radix tree
|
// Node represents a node in the radix tree
|
||||||
|
@@ -7,8 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/data/ourdb"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JobStatus represents the status of a job
|
// JobStatus represents the status of a job
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
"git.threefold.info/herocode/heroagent/pkg/tools"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/logger"
|
"git.threefold.info/herocode/heroagent/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
117
pkg/openrpc/manager.go
Normal file
117
pkg/openrpc/manager.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package openrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/openrpc/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ORPCManager manages OpenRPC specifications
|
||||||
|
type ORPCManager struct {
|
||||||
|
specs map[string]*models.OpenRPCSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewORPCManager creates a new OpenRPC Manager
|
||||||
|
func NewORPCManager() *ORPCManager {
|
||||||
|
return &ORPCManager{
|
||||||
|
specs: make(map[string]*models.OpenRPCSpec),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSpecs loads all OpenRPC specifications from a directory
|
||||||
|
func (m *ORPCManager) LoadSpecs(dir string) error {
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(file.Name(), ".json") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(dir, file.Name())
|
||||||
|
if err := m.LoadSpec(path); err != nil {
|
||||||
|
return fmt.Errorf("failed to load spec %s: %w", file.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSpec loads an OpenRPC specification from a file
|
||||||
|
func (m *ORPCManager) LoadSpec(path string) error {
|
||||||
|
// Read the file
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON
|
||||||
|
var spec models.OpenRPCSpec
|
||||||
|
if err := json.Unmarshal(data, &spec); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the specification
|
||||||
|
if err := spec.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid specification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the specification
|
||||||
|
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||||
|
m.specs[name] = &spec
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpec returns an OpenRPC specification by name
|
||||||
|
func (m *ORPCManager) GetSpec(name string) *models.OpenRPCSpec {
|
||||||
|
return m.specs[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSpecs returns a list of all loaded specification names
|
||||||
|
func (m *ORPCManager) ListSpecs() []string {
|
||||||
|
var names []string
|
||||||
|
for name := range m.specs {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMethods returns a list of all method names in a specification
|
||||||
|
func (m *ORPCManager) ListMethods(specName string) []string {
|
||||||
|
spec := m.GetSpec(specName)
|
||||||
|
if spec == nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var methods []string
|
||||||
|
for _, method := range spec.Methods {
|
||||||
|
methods = append(methods, method.Name)
|
||||||
|
}
|
||||||
|
return methods
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMethod returns a method from a specification
|
||||||
|
func (m *ORPCManager) GetMethod(specName, methodName string) *models.Method {
|
||||||
|
spec := m.GetSpec(specName)
|
||||||
|
if spec == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, method := range spec.Methods {
|
||||||
|
if method.Name == methodName {
|
||||||
|
return &method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
89
pkg/openrpc/models/spec.go
Normal file
89
pkg/openrpc/models/spec.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// OpenRPCSpec represents an OpenRPC specification document
|
||||||
|
type OpenRPCSpec struct {
|
||||||
|
OpenRPC string `json:"openrpc"`
|
||||||
|
Info InfoObject `json:"info"`
|
||||||
|
Servers []Server `json:"servers"`
|
||||||
|
Methods []Method `json:"methods"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InfoObject contains metadata about the API
|
||||||
|
type InfoObject struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
License *LicenseObject `json:"license,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LicenseObject contains license information for the API
|
||||||
|
type LicenseObject struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server represents a server that provides the API
|
||||||
|
type Server struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method represents a method in the API
|
||||||
|
type Method struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Params []Parameter `json:"params"`
|
||||||
|
Result ResultObject `json:"result"`
|
||||||
|
Examples []Example `json:"examples,omitempty"`
|
||||||
|
Errors []ErrorObject `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter represents a parameter for a method
|
||||||
|
type Parameter struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Schema SchemaObject `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultObject represents the result of a method
|
||||||
|
type ResultObject struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Schema SchemaObject `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaObject represents a JSON Schema object
|
||||||
|
type SchemaObject struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Properties map[string]SchemaObject `json:"properties,omitempty"`
|
||||||
|
Items *SchemaObject `json:"items,omitempty"`
|
||||||
|
AdditionalProperties *SchemaObject `json:"additionalProperties,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Enum []string `json:"enum,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example represents an example for a method
|
||||||
|
type Example struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Params []map[string]interface{} `json:"params"`
|
||||||
|
Result ExampleResultObject `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleResultObject represents the result of an example
|
||||||
|
type ExampleResultObject struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorObject represents an error that can be returned by a method
|
||||||
|
type ErrorObject struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data string `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the OpenRPC specification
|
||||||
|
func (spec *OpenRPCSpec) Validate() error {
|
||||||
|
// TODO: Implement validation logic
|
||||||
|
return nil
|
||||||
|
}
|
873
pkg/openrpc/services/zinit.json
Normal file
873
pkg/openrpc/services/zinit.json
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
{
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API",
|
||||||
|
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
|
||||||
|
"license": {
|
||||||
|
"name": "MIT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"name": "Unix Socket",
|
||||||
|
"url": "unix:///tmp/zinit.sock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "rpc_discover",
|
||||||
|
"description": "Returns the OpenRPC specification for the API",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpec",
|
||||||
|
"description": "The OpenRPC specification",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get API specification",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpecResult",
|
||||||
|
"value": {
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_list",
|
||||||
|
"description": "Lists all services managed by Zinit",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceList",
|
||||||
|
"description": "A map of service names to their current states",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service state (Running, Success, Error, etc.)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "List all services",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceListResult",
|
||||||
|
"value": {
|
||||||
|
"service1": "Running",
|
||||||
|
"service2": "Success",
|
||||||
|
"service3": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_status",
|
||||||
|
"description": "Shows detailed status information for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatus",
|
||||||
|
"description": "Detailed status information for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the running service (if running)"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Current state of the service (Running, Success, Error, etc.)"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target state of the service (Up, Down)"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Dependencies of the service and their states",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "State of the dependency"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get status of redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatusResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"state": "Running",
|
||||||
|
"target": "Up",
|
||||||
|
"after": {
|
||||||
|
"dependency1": "Success",
|
||||||
|
"dependency2": "Running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_start",
|
||||||
|
"description": "Starts a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to start",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"description": "Result of the start operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stop",
|
||||||
|
"description": "Stops a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to stop",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"description": "Result of the stop operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_monitor",
|
||||||
|
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to monitor",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"description": "Result of the monitor operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Monitor redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32001,
|
||||||
|
"message": "Service already monitored",
|
||||||
|
"data": "service \"redis\" already monitored"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32005,
|
||||||
|
"message": "Config error",
|
||||||
|
"data": "failed to load service configuration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_forget",
|
||||||
|
"description": "Stops monitoring a service. You can only forget a stopped service.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to forget",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"description": "Result of the forget operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Forget redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32002,
|
||||||
|
"message": "Service is up",
|
||||||
|
"data": "service \"redis\" is up"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_kill",
|
||||||
|
"description": "Sends a signal to a running service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to send the signal to",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"description": "Result of the kill operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Send SIGTERM to redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"value": "SIGTERM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32004,
|
||||||
|
"message": "Invalid signal",
|
||||||
|
"data": "invalid signal: INVALID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_shutdown",
|
||||||
|
"description": "Stops all services and powers off the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"description": "Result of the shutdown operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Shutdown the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_reboot",
|
||||||
|
"description": "Stops all services and reboots the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"description": "Result of the reboot operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Reboot the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_create",
|
||||||
|
"description": "Creates a new service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to create",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"description": "The service configuration content",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exec": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Command to run"
|
||||||
|
},
|
||||||
|
"oneshot": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the service should be restarted"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Services that must be running before this one starts"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["null", "ring", "stdout"],
|
||||||
|
"description": "How to handle service output"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Environment variables for the service"
|
||||||
|
},
|
||||||
|
"shutdown_timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum time to wait for service to stop during shutdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "CreateServiceResult",
|
||||||
|
"description": "Result of the create operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32007,
|
||||||
|
"message": "Service already exists",
|
||||||
|
"data": "Service 'name' already exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to create service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_delete",
|
||||||
|
"description": "Deletes a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to delete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "DeleteServiceResult",
|
||||||
|
"description": "Result of the delete operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to delete service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_get",
|
||||||
|
"description": "Gets a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "GetServiceResult",
|
||||||
|
"description": "The service configuration",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to read service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stats",
|
||||||
|
"description": "Get memory and CPU usage statistics for a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get stats for",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStats",
|
||||||
|
"description": "Memory and CPU usage statistics for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the service"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
},
|
||||||
|
"children": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Stats for child processes",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the child process"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get stats for redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatsResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"memory_usage": 10485760,
|
||||||
|
"cpu_usage": 2.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pid": 1235,
|
||||||
|
"memory_usage": 5242880,
|
||||||
|
"cpu_usage": 1.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_start_http_server",
|
||||||
|
"description": "Start an HTTP/RPC server at the specified address",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"description": "Result of the start HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start HTTP server on localhost:8080",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"value": "127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"value": "HTTP server started at 127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Invalid address",
|
||||||
|
"data": "Invalid network address format"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_stop_http_server",
|
||||||
|
"description": "Stop the HTTP/RPC server if running",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"description": "Result of the stop HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop the HTTP server",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Server not running",
|
||||||
|
"data": "No HTTP server is currently running"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_currentLogs",
|
||||||
|
"description": "Get current logs from zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"description": "Array of log strings",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:01 nginx: Starting service"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:02 redis: Service started"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_subscribeLogs",
|
||||||
|
"description": "Subscribe to log messages generated by zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"description": "A subscription to log messages",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Subscribe to all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Subscribe to filtered logs",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -3,8 +3,8 @@ package heroagent
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui"
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/webdavserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/webdavserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds the configuration for the HeroAgent server factory
|
// Config holds the configuration for the HeroAgent server factory
|
||||||
|
@@ -5,9 +5,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/redisserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/redisserver"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui"
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/webdavserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/webdavserver"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/data/ourdb"
|
"git.threefold.info/herocode/heroagent/pkg/data/ourdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JobManager handles job management between OurDB and Redis
|
// JobManager handles job management between OurDB and Redis
|
||||||
|
@@ -24,7 +24,7 @@ The server implements the following Redis commands:
|
|||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.ourworld.tf/herocode/heroagent/pkg/redisserver"
|
import "git.threefold.info/herocode/heroagent/pkg/redisserver"
|
||||||
|
|
||||||
// Create a new server with default configuration
|
// Create a new server with default configuration
|
||||||
server := redisserver.NewServer(redisserver.ServerConfig{
|
server := redisserver.NewServer(redisserver.ServerConfig{
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/redisserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/redisserver"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/redisserver"
|
"git.threefold.info/herocode/heroagent/pkg/servers/redisserver"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui/routes" // Import the routes package
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/routes" // Import the routes package
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
jetadapter "github.com/gofiber/template/jet/v2" // Aliased for clarity
|
jetadapter "github.com/gofiber/template/jet/v2" // Aliased for clarity
|
||||||
)
|
)
|
||||||
@@ -54,9 +58,86 @@ func NewApp(config AppConfig) *fiber.App {
|
|||||||
// Enable template reloading for development
|
// Enable template reloading for development
|
||||||
engine.Reload(true)
|
engine.Reload(true)
|
||||||
|
|
||||||
// Create a new Fiber app with the configured Jet engine
|
// No custom functions for now
|
||||||
|
|
||||||
|
// Create a new Fiber app with the configured Jet engine and enhanced error handling
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
Views: engine,
|
Views: engine,
|
||||||
|
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||||
|
// Log the detailed error
|
||||||
|
log.Printf("ERROR: %v", err)
|
||||||
|
|
||||||
|
// Check if it's a template rendering error
|
||||||
|
if err.Error() != "" && (c.Route().Path != "" && c.Method() == "GET") {
|
||||||
|
// Extract template name and line number from error message
|
||||||
|
errorMsg := err.Error()
|
||||||
|
templateInfo := "Unknown template"
|
||||||
|
lineInfo := "Unknown line"
|
||||||
|
variableInfo := "Unknown variable"
|
||||||
|
|
||||||
|
// Try to extract template name and line number
|
||||||
|
if strings.Contains(errorMsg, "Jet Runtime Error") {
|
||||||
|
// Extract template and line number
|
||||||
|
templateLineRegex := regexp.MustCompile(`"([^"]+)":(\d+)`)
|
||||||
|
templateMatches := templateLineRegex.FindStringSubmatch(errorMsg)
|
||||||
|
if len(templateMatches) >= 3 {
|
||||||
|
templateInfo = templateMatches[1]
|
||||||
|
lineInfo = templateMatches[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract variable name
|
||||||
|
varRegex := regexp.MustCompile(`there is no field or method '([^']+)'`)
|
||||||
|
varMatches := varRegex.FindStringSubmatch(errorMsg)
|
||||||
|
if len(varMatches) >= 2 {
|
||||||
|
variableInfo = varMatches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log more detailed information
|
||||||
|
log.Printf("Template Error Details - Template: %s, Line: %s, Variable: %s",
|
||||||
|
templateInfo, lineInfo, variableInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a more detailed error page
|
||||||
|
errorHTML := fmt.Sprintf(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Template Error</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.error-box { background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; }
|
||||||
|
.error-details { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 15px; border-radius: 5px; margin-top: 20px; }
|
||||||
|
.code-context { background-color: #f0f0f0; padding: 10px; border-radius: 5px; font-family: monospace; }
|
||||||
|
pre { white-space: pre-wrap; }
|
||||||
|
.highlight { background-color: #ffeb3b; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Template Error</h1>
|
||||||
|
<div class="error-box">
|
||||||
|
<h3>Error Details:</h3>
|
||||||
|
<p><strong>Template:</strong> %s</p>
|
||||||
|
<p><strong>Line:</strong> %s</p>
|
||||||
|
<p><strong>Missing Variable:</strong> <span class="highlight">%s</span></p>
|
||||||
|
<pre>%s</pre>
|
||||||
|
</div>
|
||||||
|
<div class="error-details">
|
||||||
|
<h3>Debugging Tips:</h3>
|
||||||
|
<p>1. Check if the variable <span class="highlight">%s</span> is passed to the template</p>
|
||||||
|
<p>2. Visit <a href="/debug">/debug</a> to see available template variables</p>
|
||||||
|
<p>3. Check for typos in variable names</p>
|
||||||
|
<p>4. Ensure the variable is of the expected type</p>
|
||||||
|
<p>5. Check the controller that renders this template to ensure all required data is provided</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, templateInfo, lineInfo, variableInfo, errorMsg, variableInfo)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusInternalServerError).Type("html").SendString(errorHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, use the default error handler
|
||||||
|
return fiber.DefaultErrorHandler(c, err)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup static file serving
|
// Setup static file serving
|
||||||
|
271
pkg/servers/ui/controllers/job_controller.go
Normal file
271
pkg/servers/ui/controllers/job_controller.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/herojobs"
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobController handles requests related to job management
|
||||||
|
type JobController struct {
|
||||||
|
jobManager models.JobManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJobController creates a new instance of JobController
|
||||||
|
func NewJobController(jobManager models.JobManager) *JobController {
|
||||||
|
return &JobController{
|
||||||
|
jobManager: jobManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowJobsPage renders the jobs management page
|
||||||
|
func (jc *JobController) ShowJobsPage(c *fiber.Ctx) error {
|
||||||
|
// Get filter parameters
|
||||||
|
circleID := c.Query("circle", "")
|
||||||
|
topic := c.Query("topic", "")
|
||||||
|
status := c.Query("status", "")
|
||||||
|
|
||||||
|
var jobs []*models.JobInfo
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if circleID != "" {
|
||||||
|
jobs, err = jc.jobManager.GetJobsByCircle(circleID)
|
||||||
|
} else if topic != "" {
|
||||||
|
jobs, err = jc.jobManager.GetJobsByTopic(topic)
|
||||||
|
} else if status != "" {
|
||||||
|
// Convert status string to JobStatus
|
||||||
|
var jobStatus herojobs.JobStatus
|
||||||
|
switch status {
|
||||||
|
case "new":
|
||||||
|
jobStatus = herojobs.JobStatusNew
|
||||||
|
case "active":
|
||||||
|
jobStatus = herojobs.JobStatusActive
|
||||||
|
case "error":
|
||||||
|
jobStatus = herojobs.JobStatusError
|
||||||
|
case "done":
|
||||||
|
jobStatus = herojobs.JobStatusDone
|
||||||
|
default:
|
||||||
|
// Invalid status, get all jobs
|
||||||
|
jobs, err = jc.jobManager.GetAllJobs()
|
||||||
|
}
|
||||||
|
|
||||||
|
if jobStatus != "" {
|
||||||
|
jobs, err = jc.jobManager.GetJobsByStatus(jobStatus)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No filters, get all jobs
|
||||||
|
jobs, err = jc.jobManager.GetAllJobs()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching jobs: %v", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).Render("pages/error", fiber.Map{
|
||||||
|
"Title": "Error",
|
||||||
|
"Message": "Failed to fetch jobs: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group jobs by circle and topic for tree view
|
||||||
|
jobTree := buildJobTree(jobs)
|
||||||
|
|
||||||
|
// Get unique circles and topics for filter dropdowns
|
||||||
|
circles := getUniqueCircles(jobs)
|
||||||
|
topics := getUniqueTopics(jobs)
|
||||||
|
|
||||||
|
// Create circle options with selected state
|
||||||
|
circleOptions := make([]map[string]interface{}, 0, len(circles))
|
||||||
|
for _, circle := range circles {
|
||||||
|
circleOptions = append(circleOptions, map[string]interface{}{
|
||||||
|
"Value": circle,
|
||||||
|
"Text": circle,
|
||||||
|
"Selected": circle == circleID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create topic options with selected state
|
||||||
|
topicOptions := make([]map[string]interface{}, 0, len(topics))
|
||||||
|
for _, topicName := range topics {
|
||||||
|
topicOptions = append(topicOptions, map[string]interface{}{
|
||||||
|
"Value": topicName,
|
||||||
|
"Text": topicName,
|
||||||
|
"Selected": topicName == topic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create status options with selected state
|
||||||
|
statusOptions := []map[string]interface{}{
|
||||||
|
{"Value": "new", "Text": "New", "Selected": status == "new"},
|
||||||
|
{"Value": "active", "Text": "Active", "Selected": status == "active"},
|
||||||
|
{"Value": "done", "Text": "Done", "Selected": status == "done"},
|
||||||
|
{"Value": "error", "Text": "Error", "Selected": status == "error"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map options to OptionData structs
|
||||||
|
circleOptionData := make([]OptionData, 0, len(circleOptions))
|
||||||
|
for _, option := range circleOptions {
|
||||||
|
circleOptionData = append(circleOptionData, OptionData{
|
||||||
|
Value: option["Value"].(string),
|
||||||
|
Text: option["Text"].(string),
|
||||||
|
Selected: option["Selected"].(bool),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
topicOptionData := make([]OptionData, 0, len(topicOptions))
|
||||||
|
for _, option := range topicOptions {
|
||||||
|
topicOptionData = append(topicOptionData, OptionData{
|
||||||
|
Value: option["Value"].(string),
|
||||||
|
Text: option["Text"].(string),
|
||||||
|
Selected: option["Selected"].(bool),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOptionData := make([]OptionData, 0, len(statusOptions))
|
||||||
|
for _, option := range statusOptions {
|
||||||
|
statusOptionData = append(statusOptionData, OptionData{
|
||||||
|
Value: option["Value"].(string),
|
||||||
|
Text: option["Text"].(string),
|
||||||
|
Selected: option["Selected"].(bool),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create JobPageData struct for the template
|
||||||
|
pageData := JobPageData{
|
||||||
|
Title: "Job Management",
|
||||||
|
Jobs: jobs,
|
||||||
|
JobTree: jobTree,
|
||||||
|
CircleOptions: circleOptionData,
|
||||||
|
TopicOptions: topicOptionData,
|
||||||
|
StatusOptions: statusOptionData,
|
||||||
|
FilterCircle: circleID,
|
||||||
|
FilterTopic: topic,
|
||||||
|
FilterStatus: status,
|
||||||
|
TotalJobs: len(jobs),
|
||||||
|
ActiveJobs: countJobsByStatus(jobs, herojobs.JobStatusActive),
|
||||||
|
CompletedJobs: countJobsByStatus(jobs, herojobs.JobStatusDone),
|
||||||
|
ErrorJobs: countJobsByStatus(jobs, herojobs.JobStatusError),
|
||||||
|
NewJobs: countJobsByStatus(jobs, herojobs.JobStatusNew),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the template with the structured data
|
||||||
|
return c.Render("pages/jobs", pageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowJobDetails renders the job details page
|
||||||
|
func (jc *JobController) ShowJobDetails(c *fiber.Ctx) error {
|
||||||
|
// Get job ID from URL parameter
|
||||||
|
jobIDStr := c.Params("id")
|
||||||
|
jobID, err := strconv.ParseUint(jobIDStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).Render("pages/error", fiber.Map{
|
||||||
|
"Title": "Error",
|
||||||
|
"Message": "Invalid job ID: " + jobIDStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get job details
|
||||||
|
job, err := jc.jobManager.GetJob(uint32(jobID))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).Render("pages/error", fiber.Map{
|
||||||
|
"Title": "Error",
|
||||||
|
"Message": "Job not found: " + jobIDStr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("pages/job_details", fiber.Map{
|
||||||
|
"Title": "Job Details",
|
||||||
|
"Job": job,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircleNode represents a circle in the job tree
|
||||||
|
type CircleNode struct {
|
||||||
|
CircleID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Topics map[string]*TopicNode `json:"topics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopicNode represents a topic in the job tree
|
||||||
|
type TopicNode struct {
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Jobs []*models.JobInfo `json:"jobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildJobTree groups jobs by circle and topic for the tree view
|
||||||
|
func buildJobTree(jobs []*models.JobInfo) map[string]*CircleNode {
|
||||||
|
tree := make(map[string]*CircleNode)
|
||||||
|
|
||||||
|
for _, job := range jobs {
|
||||||
|
// Get or create circle node
|
||||||
|
circle, exists := tree[job.CircleID]
|
||||||
|
if !exists {
|
||||||
|
circle = &CircleNode{
|
||||||
|
CircleID: job.CircleID,
|
||||||
|
Name: job.CircleID, // Use CircleID as name for now
|
||||||
|
Topics: make(map[string]*TopicNode),
|
||||||
|
}
|
||||||
|
tree[job.CircleID] = circle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create topic node
|
||||||
|
topic, exists := circle.Topics[job.Topic]
|
||||||
|
if !exists {
|
||||||
|
topic = &TopicNode{
|
||||||
|
Topic: job.Topic,
|
||||||
|
Name: job.Topic, // Use Topic as name for now
|
||||||
|
Jobs: make([]*models.JobInfo, 0),
|
||||||
|
}
|
||||||
|
circle.Topics[job.Topic] = topic
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add job to topic
|
||||||
|
topic.Jobs = append(topic.Jobs, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUniqueCircles returns a list of unique circle IDs from jobs
|
||||||
|
func getUniqueCircles(jobs []*models.JobInfo) []string {
|
||||||
|
circleMap := make(map[string]bool)
|
||||||
|
for _, job := range jobs {
|
||||||
|
circleMap[job.CircleID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
circles := make([]string, 0, len(circleMap))
|
||||||
|
for circle := range circleMap {
|
||||||
|
circles = append(circles, circle)
|
||||||
|
}
|
||||||
|
|
||||||
|
return circles
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUniqueTopics returns a list of unique topics from jobs
|
||||||
|
func getUniqueTopics(jobs []*models.JobInfo) []string {
|
||||||
|
topicMap := make(map[string]bool)
|
||||||
|
for _, job := range jobs {
|
||||||
|
topicMap[job.Topic] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
topics := make([]string, 0, len(topicMap))
|
||||||
|
for topic := range topicMap {
|
||||||
|
topics = append(topics, topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
return topics
|
||||||
|
}
|
||||||
|
|
||||||
|
// countJobsByStatus counts jobs with a specific status
|
||||||
|
func countJobsByStatus(jobs []*models.JobInfo, status herojobs.JobStatus) int {
|
||||||
|
count := 0
|
||||||
|
for _, job := range jobs {
|
||||||
|
if job.Status == status {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
30
pkg/servers/ui/controllers/job_page_data.go
Normal file
30
pkg/servers/ui/controllers/job_page_data.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobPageData represents the data needed for the jobs page
|
||||||
|
type JobPageData struct {
|
||||||
|
Title string
|
||||||
|
Jobs []*models.JobInfo
|
||||||
|
JobTree map[string]*CircleNode
|
||||||
|
CircleOptions []OptionData
|
||||||
|
TopicOptions []OptionData
|
||||||
|
StatusOptions []OptionData
|
||||||
|
FilterCircle string
|
||||||
|
FilterTopic string
|
||||||
|
FilterStatus string
|
||||||
|
TotalJobs int
|
||||||
|
ActiveJobs int
|
||||||
|
CompletedJobs int
|
||||||
|
ErrorJobs int
|
||||||
|
NewJobs int
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionData represents a select option
|
||||||
|
type OptionData struct {
|
||||||
|
Value string
|
||||||
|
Text string
|
||||||
|
Selected bool
|
||||||
|
}
|
173
pkg/servers/ui/controllers/openrpc_controller.go
Normal file
173
pkg/servers/ui/controllers/openrpc_controller.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
orpcmodels "git.threefold.info/herocode/heroagent/pkg/openrpc/models"
|
||||||
|
uimodels "git.threefold.info/herocode/heroagent/pkg/servers/ui/models"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenRPCController handles requests related to OpenRPC specifications
|
||||||
|
type OpenRPCController struct {
|
||||||
|
openrpcManager uimodels.OpenRPCUIManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenRPCController creates a new instance of OpenRPCController
|
||||||
|
func NewOpenRPCController(openrpcManager uimodels.OpenRPCUIManager) *OpenRPCController {
|
||||||
|
return &OpenRPCController{
|
||||||
|
openrpcManager: openrpcManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenRPCPageData represents the data needed for the OpenRPC UI pages
|
||||||
|
type OpenRPCPageData struct {
|
||||||
|
Title string
|
||||||
|
Specs []string
|
||||||
|
SelectedSpec string
|
||||||
|
Methods []string
|
||||||
|
SelectedMethod string
|
||||||
|
Method *orpcmodels.Method
|
||||||
|
SocketPath string
|
||||||
|
ExampleParams string
|
||||||
|
Result string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowOpenRPCUI renders the OpenRPC UI page
|
||||||
|
func (c *OpenRPCController) ShowOpenRPCUI(ctx *fiber.Ctx) error {
|
||||||
|
// Get query parameters
|
||||||
|
selectedSpec := ctx.Query("spec", "")
|
||||||
|
selectedMethod := ctx.Query("method", "")
|
||||||
|
socketPath := ctx.Query("socketPath", "")
|
||||||
|
|
||||||
|
// Get all specs
|
||||||
|
specs := c.openrpcManager.ListSpecs()
|
||||||
|
|
||||||
|
// Initialize page data using fiber.Map instead of struct
|
||||||
|
pageData := fiber.Map{
|
||||||
|
"Title": "OpenRPC UI",
|
||||||
|
"SpecList": specs,
|
||||||
|
"SelectedSpec": selectedSpec,
|
||||||
|
"SocketPath": socketPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a spec is selected, get its methods
|
||||||
|
if selectedSpec != "" {
|
||||||
|
methods := c.openrpcManager.ListMethods(selectedSpec)
|
||||||
|
pageData["Methods"] = methods
|
||||||
|
pageData["SelectedMethod"] = selectedMethod
|
||||||
|
|
||||||
|
// If a method is selected, get its details
|
||||||
|
if selectedMethod != "" {
|
||||||
|
method := c.openrpcManager.GetMethod(selectedSpec, selectedMethod)
|
||||||
|
if method != nil {
|
||||||
|
pageData["Method"] = method
|
||||||
|
|
||||||
|
// Generate example parameters if available
|
||||||
|
if len(method.Examples) > 0 {
|
||||||
|
exampleParams, err := json.MarshalIndent(method.Examples[0].Params, "", " ")
|
||||||
|
if err == nil {
|
||||||
|
pageData["ExampleParams"] = string(exampleParams)
|
||||||
|
}
|
||||||
|
} else if len(method.Params) > 0 {
|
||||||
|
// Generate example from parameter schema
|
||||||
|
exampleParams := generateExampleParams(method.Params)
|
||||||
|
jsonParams, err := json.MarshalIndent(exampleParams, "", " ")
|
||||||
|
if err == nil {
|
||||||
|
pageData["ExampleParams"] = string(jsonParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Render("pages/rpcui", pageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteRPC handles RPC execution requests
|
||||||
|
func (c *OpenRPCController) ExecuteRPC(ctx *fiber.Ctx) error {
|
||||||
|
// Parse request
|
||||||
|
var request struct {
|
||||||
|
Spec string `json:"spec"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
SocketPath string `json:"socketPath"`
|
||||||
|
Params json.RawMessage `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.BodyParser(&request); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Invalid request: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if request.Spec == "" || request.Method == "" || request.SocketPath == "" {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Missing required fields: spec, method, or socketPath",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse params
|
||||||
|
var params interface{}
|
||||||
|
if len(request.Params) > 0 {
|
||||||
|
if err := json.Unmarshal(request.Params, ¶ms); err != nil {
|
||||||
|
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"error": "Invalid parameters: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute RPC
|
||||||
|
result, err := c.openrpcManager.ExecuteRPC(request.Spec, request.Method, request.SocketPath, params)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error executing RPC: %v", err)
|
||||||
|
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return ctx.JSON(fiber.Map{
|
||||||
|
"result": result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateExampleParams generates example parameters from parameter schemas
|
||||||
|
func generateExampleParams(params []orpcmodels.Parameter) map[string]interface{} {
|
||||||
|
example := make(map[string]interface{})
|
||||||
|
|
||||||
|
for _, param := range params {
|
||||||
|
example[param.Name] = generateExampleValue(param.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return example
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateExampleValue generates an example value from a schema
|
||||||
|
func generateExampleValue(schema orpcmodels.SchemaObject) interface{} {
|
||||||
|
switch schema.Type {
|
||||||
|
case "string":
|
||||||
|
return "example"
|
||||||
|
case "number":
|
||||||
|
return 0
|
||||||
|
case "integer":
|
||||||
|
return 0
|
||||||
|
case "boolean":
|
||||||
|
return false
|
||||||
|
case "array":
|
||||||
|
if schema.Items != nil {
|
||||||
|
return []interface{}{generateExampleValue(*schema.Items)}
|
||||||
|
}
|
||||||
|
return []interface{}{}
|
||||||
|
case "object":
|
||||||
|
obj := make(map[string]interface{})
|
||||||
|
for name, propSchema := range schema.Properties {
|
||||||
|
obj[name] = generateExampleValue(propSchema)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui/models"
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/models"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
348
pkg/servers/ui/models/job_manager.go
Normal file
348
pkg/servers/ui/models/job_manager.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/herojobs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobManager provides an interface for job management operations
|
||||||
|
type JobManager interface {
|
||||||
|
// GetAllJobs returns all jobs
|
||||||
|
GetAllJobs() ([]*JobInfo, error)
|
||||||
|
// GetJobsByCircle returns jobs for a specific circle
|
||||||
|
GetJobsByCircle(circleID string) ([]*JobInfo, error)
|
||||||
|
// GetJobsByTopic returns jobs for a specific topic
|
||||||
|
GetJobsByTopic(topic string) ([]*JobInfo, error)
|
||||||
|
// GetJobsByStatus returns jobs with a specific status
|
||||||
|
GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error)
|
||||||
|
// GetJob returns a specific job by ID
|
||||||
|
GetJob(jobID uint32) (*JobInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobInfo represents job information for the UI
|
||||||
|
type JobInfo struct {
|
||||||
|
JobID uint32 `json:"jobid"`
|
||||||
|
SessionKey string `json:"sessionkey"`
|
||||||
|
CircleID string `json:"circleid"`
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
ParamsType herojobs.ParamsType `json:"params_type"`
|
||||||
|
Status herojobs.JobStatus `json:"status"`
|
||||||
|
TimeScheduled time.Time `json:"time_scheduled"`
|
||||||
|
TimeStart time.Time `json:"time_start"`
|
||||||
|
TimeEnd time.Time `json:"time_end"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
HasError bool `json:"has_error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeroJobManager implements JobManager interface using herojobs package
|
||||||
|
type HeroJobManager struct {
|
||||||
|
factory *herojobs.Factory
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHeroJobManager creates a new HeroJobManager
|
||||||
|
func NewHeroJobManager(redisURL string) (*HeroJobManager, error) {
|
||||||
|
factory, err := herojobs.NewFactory(redisURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create job factory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HeroJobManager{
|
||||||
|
factory: factory,
|
||||||
|
ctx: context.Background(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllJobs returns all jobs from Redis
|
||||||
|
func (jm *HeroJobManager) GetAllJobs() ([]*JobInfo, error) {
|
||||||
|
// This is a simplified implementation
|
||||||
|
// In a real-world scenario, you would need to:
|
||||||
|
// 1. Get all circles and topics
|
||||||
|
// 2. For each circle/topic combination, get all jobs
|
||||||
|
// 3. Combine the results
|
||||||
|
|
||||||
|
// For now, we'll just list all job IDs from Redis
|
||||||
|
jobIDs, err := jm.factory.ListJobs(jm.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := make([]*JobInfo, 0, len(jobIDs))
|
||||||
|
for _, jobID := range jobIDs {
|
||||||
|
// Extract job ID from the key
|
||||||
|
// Assuming the key format is "job:<id>"
|
||||||
|
jobData, err := jm.factory.GetJob(jm.ctx, jobID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: failed to get job %s: %v", jobID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse job data
|
||||||
|
job, err := herojobs.NewJobFromJSON(jobData)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: failed to parse job data for %s: %v", jobID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to JobInfo
|
||||||
|
jobInfo := convertToJobInfo(job)
|
||||||
|
jobs = append(jobs, jobInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByCircle returns jobs for a specific circle
|
||||||
|
func (jm *HeroJobManager) GetJobsByCircle(circleID string) ([]*JobInfo, error) {
|
||||||
|
// Implementation would filter jobs by circle ID
|
||||||
|
// For now, return all jobs and filter in memory
|
||||||
|
allJobs, err := jm.GetAllJobs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.CircleID == circleID {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByTopic returns jobs for a specific topic
|
||||||
|
func (jm *HeroJobManager) GetJobsByTopic(topic string) ([]*JobInfo, error) {
|
||||||
|
// Implementation would filter jobs by topic
|
||||||
|
// For now, return all jobs and filter in memory
|
||||||
|
allJobs, err := jm.GetAllJobs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.Topic == topic {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByStatus returns jobs with a specific status
|
||||||
|
func (jm *HeroJobManager) GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error) {
|
||||||
|
// Implementation would filter jobs by status
|
||||||
|
// For now, return all jobs and filter in memory
|
||||||
|
allJobs, err := jm.GetAllJobs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.Status == status {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJob returns a specific job by ID
|
||||||
|
func (jm *HeroJobManager) GetJob(jobID uint32) (*JobInfo, error) {
|
||||||
|
// Implementation would get a specific job by ID
|
||||||
|
// This is a placeholder implementation
|
||||||
|
allJobs, err := jm.GetAllJobs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.JobID == jobID {
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("job not found: %d", jobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToJobInfo converts a herojobs.Job to a JobInfo
|
||||||
|
func convertToJobInfo(job *herojobs.Job) *JobInfo {
|
||||||
|
// Convert Unix timestamps to time.Time
|
||||||
|
timeScheduled := time.Unix(job.TimeScheduled, 0)
|
||||||
|
timeStart := time.Time{}
|
||||||
|
if job.TimeStart > 0 {
|
||||||
|
timeStart = time.Unix(job.TimeStart, 0)
|
||||||
|
}
|
||||||
|
timeEnd := time.Time{}
|
||||||
|
if job.TimeEnd > 0 {
|
||||||
|
timeEnd = time.Unix(job.TimeEnd, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
var duration string
|
||||||
|
if job.TimeStart > 0 {
|
||||||
|
if job.TimeEnd > 0 {
|
||||||
|
// Job has completed
|
||||||
|
duration = time.Unix(job.TimeEnd, 0).Sub(time.Unix(job.TimeStart, 0)).String()
|
||||||
|
} else {
|
||||||
|
// Job is still running
|
||||||
|
duration = time.Since(time.Unix(job.TimeStart, 0)).String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
duration = "Not started"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &JobInfo{
|
||||||
|
JobID: job.JobID,
|
||||||
|
SessionKey: job.SessionKey,
|
||||||
|
CircleID: job.CircleID,
|
||||||
|
Topic: job.Topic,
|
||||||
|
ParamsType: job.ParamsType,
|
||||||
|
Status: job.Status,
|
||||||
|
TimeScheduled: timeScheduled,
|
||||||
|
TimeStart: timeStart,
|
||||||
|
TimeEnd: timeEnd,
|
||||||
|
Duration: duration,
|
||||||
|
Error: job.Error,
|
||||||
|
HasError: job.Error != "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockJobManager is a mock implementation of JobManager for testing
|
||||||
|
type MockJobManager struct{}
|
||||||
|
|
||||||
|
// NewMockJobManager creates a new MockJobManager
|
||||||
|
func NewMockJobManager() *MockJobManager {
|
||||||
|
return &MockJobManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllJobs returns mock jobs
|
||||||
|
func (m *MockJobManager) GetAllJobs() ([]*JobInfo, error) {
|
||||||
|
return generateMockJobs(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByCircle returns mock jobs for a circle
|
||||||
|
func (m *MockJobManager) GetJobsByCircle(circleID string) ([]*JobInfo, error) {
|
||||||
|
allJobs := generateMockJobs()
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.CircleID == circleID {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByTopic returns mock jobs for a topic
|
||||||
|
func (m *MockJobManager) GetJobsByTopic(topic string) ([]*JobInfo, error) {
|
||||||
|
allJobs := generateMockJobs()
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.Topic == topic {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJobsByStatus returns mock jobs with a status
|
||||||
|
func (m *MockJobManager) GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error) {
|
||||||
|
allJobs := generateMockJobs()
|
||||||
|
filteredJobs := make([]*JobInfo, 0)
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.Status == status {
|
||||||
|
filteredJobs = append(filteredJobs, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredJobs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJob returns a mock job by ID
|
||||||
|
func (m *MockJobManager) GetJob(jobID uint32) (*JobInfo, error) {
|
||||||
|
allJobs := generateMockJobs()
|
||||||
|
for _, job := range allJobs {
|
||||||
|
if job.JobID == jobID {
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("job not found: %d", jobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMockJobs generates mock jobs for testing
|
||||||
|
func generateMockJobs() []*JobInfo {
|
||||||
|
now := time.Now()
|
||||||
|
return []*JobInfo{
|
||||||
|
{
|
||||||
|
JobID: 1,
|
||||||
|
CircleID: "circle1",
|
||||||
|
Topic: "email",
|
||||||
|
ParamsType: herojobs.ParamsTypeHeroScript,
|
||||||
|
Status: herojobs.JobStatusDone,
|
||||||
|
TimeScheduled: now.Add(-30 * time.Minute),
|
||||||
|
TimeStart: now.Add(-29 * time.Minute),
|
||||||
|
TimeEnd: now.Add(-28 * time.Minute),
|
||||||
|
Duration: "1m0s",
|
||||||
|
Error: "",
|
||||||
|
HasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobID: 2,
|
||||||
|
CircleID: "circle1",
|
||||||
|
Topic: "backup",
|
||||||
|
ParamsType: herojobs.ParamsTypeOpenRPC,
|
||||||
|
Status: herojobs.JobStatusActive,
|
||||||
|
TimeScheduled: now.Add(-15 * time.Minute),
|
||||||
|
TimeStart: now.Add(-14 * time.Minute),
|
||||||
|
TimeEnd: time.Time{},
|
||||||
|
Duration: "14m0s",
|
||||||
|
Error: "",
|
||||||
|
HasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobID: 3,
|
||||||
|
CircleID: "circle2",
|
||||||
|
Topic: "sync",
|
||||||
|
ParamsType: herojobs.ParamsTypeRhaiScript,
|
||||||
|
Status: herojobs.JobStatusError,
|
||||||
|
TimeScheduled: now.Add(-45 * time.Minute),
|
||||||
|
TimeStart: now.Add(-44 * time.Minute),
|
||||||
|
TimeEnd: now.Add(-43 * time.Minute),
|
||||||
|
Duration: "1m0s",
|
||||||
|
Error: "Failed to connect to remote server",
|
||||||
|
HasError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobID: 4,
|
||||||
|
CircleID: "circle2",
|
||||||
|
Topic: "email",
|
||||||
|
ParamsType: herojobs.ParamsTypeHeroScript,
|
||||||
|
Status: herojobs.JobStatusNew,
|
||||||
|
TimeScheduled: now.Add(-5 * time.Minute),
|
||||||
|
TimeStart: time.Time{},
|
||||||
|
TimeEnd: time.Time{},
|
||||||
|
Duration: "Not started",
|
||||||
|
Error: "",
|
||||||
|
HasError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JobID: 5,
|
||||||
|
CircleID: "circle3",
|
||||||
|
Topic: "ai",
|
||||||
|
ParamsType: herojobs.ParamsTypeAI,
|
||||||
|
Status: herojobs.JobStatusDone,
|
||||||
|
TimeScheduled: now.Add(-60 * time.Minute),
|
||||||
|
TimeStart: now.Add(-59 * time.Minute),
|
||||||
|
TimeEnd: now.Add(-40 * time.Minute),
|
||||||
|
Duration: "19m0s",
|
||||||
|
Error: "",
|
||||||
|
HasError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
190
pkg/servers/ui/models/openrpc_manager.go
Normal file
190
pkg/servers/ui/models/openrpc_manager.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/openrpc"
|
||||||
|
"git.threefold.info/herocode/heroagent/pkg/openrpc/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenRPCUIManager is the interface for managing OpenRPC specifications in the UI
|
||||||
|
type OpenRPCUIManager interface {
|
||||||
|
// ListSpecs returns a list of all loaded specification names
|
||||||
|
ListSpecs() []string
|
||||||
|
|
||||||
|
// GetSpec returns an OpenRPC specification by name
|
||||||
|
GetSpec(name string) *models.OpenRPCSpec
|
||||||
|
|
||||||
|
// ListMethods returns a list of all method names in a specification
|
||||||
|
ListMethods(specName string) []string
|
||||||
|
|
||||||
|
// GetMethod returns a method from a specification
|
||||||
|
GetMethod(specName, methodName string) *models.Method
|
||||||
|
|
||||||
|
// ExecuteRPC executes an RPC call
|
||||||
|
ExecuteRPC(specName, methodName, socketPath string, params interface{}) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenRPCManager implements the OpenRPCUIManager interface
|
||||||
|
type OpenRPCManager struct {
|
||||||
|
manager *openrpc.ORPCManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOpenRPCManager creates a new OpenRPCUIManager
|
||||||
|
func NewOpenRPCManager() OpenRPCUIManager {
|
||||||
|
manager := openrpc.NewORPCManager()
|
||||||
|
|
||||||
|
// Try to load specs from the default directory
|
||||||
|
specDirs := []string{
|
||||||
|
"./pkg/openrpc/services",
|
||||||
|
"./pkg/openrpc/specs",
|
||||||
|
"./specs/openrpc",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range specDirs {
|
||||||
|
err := manager.LoadSpecs(dir)
|
||||||
|
if err == nil {
|
||||||
|
// Successfully loaded specs from this directory
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OpenRPCManager{
|
||||||
|
manager: manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSpecs returns a list of all loaded specification names
|
||||||
|
func (m *OpenRPCManager) ListSpecs() []string {
|
||||||
|
return m.manager.ListSpecs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpec returns an OpenRPC specification by name
|
||||||
|
func (m *OpenRPCManager) GetSpec(name string) *models.OpenRPCSpec {
|
||||||
|
return m.manager.GetSpec(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMethods returns a list of all method names in a specification
|
||||||
|
func (m *OpenRPCManager) ListMethods(specName string) []string {
|
||||||
|
return m.manager.ListMethods(specName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMethod returns a method from a specification
|
||||||
|
func (m *OpenRPCManager) GetMethod(specName, methodName string) *models.Method {
|
||||||
|
return m.manager.GetMethod(specName, methodName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteRPC executes an RPC call
|
||||||
|
func (m *OpenRPCManager) ExecuteRPC(specName, methodName, socketPath string, params interface{}) (interface{}, error) {
|
||||||
|
// Create JSON-RPC request
|
||||||
|
request := map[string]interface{}{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": methodName,
|
||||||
|
"params": params,
|
||||||
|
"id": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal request to JSON
|
||||||
|
requestJSON, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socket path is a Unix socket or HTTP endpoint
|
||||||
|
if socketPath[:1] == "/" {
|
||||||
|
// Unix socket
|
||||||
|
return executeUnixSocketRPC(socketPath, requestJSON)
|
||||||
|
} else {
|
||||||
|
// HTTP endpoint
|
||||||
|
return executeHTTPRPC(socketPath, requestJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeUnixSocketRPC executes an RPC call over a Unix socket
|
||||||
|
func executeUnixSocketRPC(socketPath string, requestJSON []byte) (interface{}, error) {
|
||||||
|
// Connect to Unix socket
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to socket %s: %w", socketPath, err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Set timeout
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
if err := conn.SetDeadline(deadline); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
if _, err := conn.Write(requestJSON); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := buf.ReadFrom(conn); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for error
|
||||||
|
if errObj, ok := response["error"]; ok {
|
||||||
|
return nil, fmt.Errorf("RPC error: %v", errObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return response["result"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeHTTPRPC executes an RPC call over HTTP
|
||||||
|
func executeHTTPRPC(endpoint string, requestJSON []byte) (interface{}, error) {
|
||||||
|
// Create HTTP request
|
||||||
|
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(requestJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Create HTTP client with timeout
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check status code
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP error: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for error
|
||||||
|
if errObj, ok := response["error"]; ok {
|
||||||
|
return nil, fmt.Errorf("RPC error: %v", errObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return response["result"], nil
|
||||||
|
}
|
@@ -1,8 +1,8 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui/controllers"
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/controllers"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/servers/ui/models"
|
"git.threefold.info/herocode/heroagent/pkg/servers/ui/models"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,10 +11,14 @@ func SetupRoutes(app *fiber.App) {
|
|||||||
// Initialize services and controllers
|
// Initialize services and controllers
|
||||||
// For now, using the mock process manager
|
// For now, using the mock process manager
|
||||||
processManagerService := models.NewMockProcessManager()
|
processManagerService := models.NewMockProcessManager()
|
||||||
|
jobManagerService := models.NewMockJobManager()
|
||||||
|
openrpcManagerService := models.NewOpenRPCManager()
|
||||||
|
|
||||||
dashboardController := controllers.NewDashboardController()
|
dashboardController := controllers.NewDashboardController()
|
||||||
processController := controllers.NewProcessController(processManagerService)
|
processController := controllers.NewProcessController(processManagerService)
|
||||||
|
jobController := controllers.NewJobController(jobManagerService)
|
||||||
authController := controllers.NewAuthController()
|
authController := controllers.NewAuthController()
|
||||||
|
openrpcController := controllers.NewOpenRPCController(openrpcManagerService)
|
||||||
|
|
||||||
// --- Public Routes ---
|
// --- Public Routes ---
|
||||||
// Login and Logout
|
// Login and Logout
|
||||||
@@ -32,9 +36,40 @@ func SetupRoutes(app *fiber.App) {
|
|||||||
|
|
||||||
// For now, routes are public for development ease
|
// For now, routes are public for development ease
|
||||||
app.Get("/", dashboardController.ShowDashboard)
|
app.Get("/", dashboardController.ShowDashboard)
|
||||||
|
|
||||||
|
// Process management routes
|
||||||
app.Get("/processes", processController.ShowProcessManager)
|
app.Get("/processes", processController.ShowProcessManager)
|
||||||
app.Post("/processes/kill/:pid", processController.HandleKillProcess)
|
app.Post("/processes/kill/:pid", processController.HandleKillProcess)
|
||||||
|
|
||||||
|
// Job management routes
|
||||||
|
app.Get("/jobs", jobController.ShowJobsPage)
|
||||||
|
app.Get("/jobs/:id", jobController.ShowJobDetails)
|
||||||
|
|
||||||
|
// OpenRPC UI routes
|
||||||
|
app.Get("/rpcui", openrpcController.ShowOpenRPCUI)
|
||||||
|
app.Post("/api/rpcui/execute", openrpcController.ExecuteRPC)
|
||||||
|
|
||||||
|
// Debug routes
|
||||||
|
app.Get("/debug", func(c *fiber.Ctx) error {
|
||||||
|
// Get all data from the jobs page to debug
|
||||||
|
jobManagerService := models.NewMockJobManager()
|
||||||
|
jobs, _ := jobManagerService.GetAllJobs()
|
||||||
|
|
||||||
|
// Create debug data
|
||||||
|
debugData := fiber.Map{
|
||||||
|
"Title": "Debug Page",
|
||||||
|
"Jobs": jobs,
|
||||||
|
"TemplateData": fiber.Map{
|
||||||
|
"TotalJobs": len(jobs),
|
||||||
|
"ActiveJobs": 0,
|
||||||
|
"CompletedJobs": 0,
|
||||||
|
"ErrorJobs": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as JSON instead of rendering a template
|
||||||
|
return c.JSON(debugData)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement authMiddleware
|
// TODO: Implement authMiddleware
|
||||||
|
167
pkg/servers/ui/static/css/jobs.css
Normal file
167
pkg/servers/ui/static/css/jobs.css
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/* Job Management UI Styles */
|
||||||
|
|
||||||
|
/* Tree View Styles */
|
||||||
|
.job-tree {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-node, .topic-node {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-header, .topic-header {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-header:hover, .topic-header:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-name, .topic-name {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-node {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-link {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-link:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topics-container, .jobs-container {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
.status-new {
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-done {
|
||||||
|
color: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Job Details Page */
|
||||||
|
.job-details-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-details-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.error-message {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #842029;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.result-content {
|
||||||
|
background-color: #d1e7dd;
|
||||||
|
color: #0f5132;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.params-content {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Job Stats Cards */
|
||||||
|
.stats-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .card-text {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Section */
|
||||||
|
.filter-section {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.job-tree {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card .card-text {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
144
pkg/servers/ui/static/css/rpcui.css
Normal file
144
pkg/servers/ui/static/css/rpcui.css
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/* OpenRPC UI Styles */
|
||||||
|
|
||||||
|
.method-tree {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-item.active {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
font-weight: bold;
|
||||||
|
border-left: 3px solid #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-card {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-table {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-required {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-optional {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-container {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-content {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Socket path input styling */
|
||||||
|
.socket-path-container {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.socket-path-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute button styling */
|
||||||
|
.execute-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Response styling */
|
||||||
|
.response-success {
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-error {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading indicator */
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 0.2em solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spinner-border .75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner-border {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
141
pkg/servers/ui/static/js/rpcui.js
Normal file
141
pkg/servers/ui/static/js/rpcui.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* OpenRPC UI JavaScript
|
||||||
|
* Handles the interactive functionality of the OpenRPC UI
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize form elements
|
||||||
|
const specForm = document.getElementById('specForm');
|
||||||
|
const rpcForm = document.getElementById('rpcForm');
|
||||||
|
const paramsEditor = document.getElementById('paramsEditor');
|
||||||
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
|
const resultOutput = document.getElementById('resultOutput');
|
||||||
|
const errorContainer = document.getElementById('errorContainer');
|
||||||
|
const errorOutput = document.getElementById('errorOutput');
|
||||||
|
|
||||||
|
// Format JSON in the parameters editor
|
||||||
|
if (paramsEditor && paramsEditor.value) {
|
||||||
|
try {
|
||||||
|
const params = JSON.parse(paramsEditor.value);
|
||||||
|
paramsEditor.value = JSON.stringify(params, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// If not valid JSON, leave as is
|
||||||
|
console.warn('Could not format parameters as JSON:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle RPC execution
|
||||||
|
if (rpcForm) {
|
||||||
|
rpcForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Hide previous results
|
||||||
|
if (resultContainer) resultContainer.classList.add('d-none');
|
||||||
|
if (errorContainer) errorContainer.classList.add('d-none');
|
||||||
|
|
||||||
|
// Get form data
|
||||||
|
const spec = document.getElementById('spec').value;
|
||||||
|
const method = document.querySelector('input[name="selectedMethod"]').value;
|
||||||
|
const socketPath = document.getElementById('socketPath').value;
|
||||||
|
const paramsText = paramsEditor.value;
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
const submitButton = rpcForm.querySelector('button[type="submit"]');
|
||||||
|
const originalButtonText = submitButton.innerHTML;
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<span class="loading-spinner me-2"></span>Executing...';
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if (!spec || !method || !socketPath) {
|
||||||
|
showError('Missing required fields: spec, method, or socketPath');
|
||||||
|
resetButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse params
|
||||||
|
let params;
|
||||||
|
try {
|
||||||
|
params = JSON.parse(paramsText);
|
||||||
|
} catch (e) {
|
||||||
|
showError('Invalid JSON parameters: ' + e.message);
|
||||||
|
resetButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute RPC
|
||||||
|
fetch('/api/rpcui/execute', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
spec: spec,
|
||||||
|
method: method,
|
||||||
|
socketPath: socketPath,
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showError(data.error);
|
||||||
|
} else {
|
||||||
|
showResult(data.result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showError('Request failed: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
resetButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetButton() {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = originalButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
if (errorContainer && errorOutput) {
|
||||||
|
errorContainer.classList.remove('d-none');
|
||||||
|
errorOutput.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(result) {
|
||||||
|
if (resultContainer && resultOutput) {
|
||||||
|
resultContainer.classList.remove('d-none');
|
||||||
|
resultOutput.textContent = JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method tree navigation
|
||||||
|
const methodItems = document.querySelectorAll('.method-item');
|
||||||
|
methodItems.forEach(item => {
|
||||||
|
item.addEventListener('click', function(e) {
|
||||||
|
// Already handled by href, but could add additional functionality here
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format JSON examples
|
||||||
|
const jsonExamples = document.querySelectorAll('pre code');
|
||||||
|
jsonExamples.forEach(example => {
|
||||||
|
try {
|
||||||
|
const content = example.textContent;
|
||||||
|
const json = JSON.parse(content);
|
||||||
|
example.textContent = JSON.stringify(json, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// If not valid JSON, leave as is
|
||||||
|
console.warn('Could not format example as JSON:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add syntax highlighting if a library like highlight.js is available
|
||||||
|
if (typeof hljs !== 'undefined') {
|
||||||
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightBlock(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@@ -12,6 +12,18 @@
|
|||||||
Process Manager
|
Process Manager
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/jobs">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-briefcase"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg>
|
||||||
|
Job Manager
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/rpcui">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
|
||||||
|
OpenRPC UI
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<!-- Add more menu items here as needed -->
|
<!-- Add more menu items here as needed -->
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
@@ -8,6 +8,8 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
|
||||||
<!-- Custom CSS -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="/static/css/custom.css">
|
<link rel="stylesheet" href="/static/css/custom.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/jobs.css">
|
||||||
|
{{ block css() }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{ include "../components/navbar" }}
|
{{ include "../components/navbar" }}
|
||||||
|
33
pkg/servers/ui/views/pages/debug.jet
Normal file
33
pkg/servers/ui/views/pages/debug.jet
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{{ extends "../layouts/base" }}
|
||||||
|
|
||||||
|
{{ block title() }}Debug Template Variables{{ end }}
|
||||||
|
|
||||||
|
{{ block body() }}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h1>Template Variables Debug</h1>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Available Variables</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre id="debug-output">{{ dump(.) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block scripts() }}
|
||||||
|
<script>
|
||||||
|
// Format the JSON for better readability
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
try {
|
||||||
|
const debugOutput = document.getElementById('debug-output');
|
||||||
|
const content = debugOutput.textContent;
|
||||||
|
const obj = JSON.parse(content);
|
||||||
|
debugOutput.textContent = JSON.stringify(obj, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse JSON:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
172
pkg/servers/ui/views/pages/job_details.jet
Normal file
172
pkg/servers/ui/views/pages/job_details.jet
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
{{ extends "../layouts/base" }}
|
||||||
|
|
||||||
|
{{ block title() }}Job Details - HeroApp UI{{ end }}
|
||||||
|
|
||||||
|
{{ block body() }}
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">Job Details</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<a href="/jobs" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||||
|
Back to Jobs
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshJobDetails()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5>Job #{{ .Job.JobID }}</h5>
|
||||||
|
<span class="badge {{ if .Job.Status == "error" }}bg-danger{{ else if .Job.Status == "done" }}bg-success{{ else if .Job.Status == "active" }}bg-warning text-dark{{ else }}bg-primary{{ end }}">
|
||||||
|
{{ .Job.Status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Job ID</th>
|
||||||
|
<td>{{ .Job.JobID }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Circle ID</th>
|
||||||
|
<td>{{ .Job.CircleID }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Topic</th>
|
||||||
|
<td>{{ .Job.Topic }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td>
|
||||||
|
<span class="badge {{ if .Job.Status == "error" }}bg-danger{{ else if .Job.Status == "done" }}bg-success{{ else if .Job.Status == "active" }}bg-warning text-dark{{ else }}bg-primary{{ end }}">
|
||||||
|
{{ .Job.Status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Parameters Type</th>
|
||||||
|
<td>{{ .Job.ParamsType }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Scheduled Time</th>
|
||||||
|
<td>{{ .Job.TimeScheduled.Format("2006-01-02 15:04:05") }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Start Time</th>
|
||||||
|
<td>
|
||||||
|
{{ if not .Job.TimeStart.IsZero }}
|
||||||
|
{{ .Job.TimeStart.Format("2006-01-02 15:04:05") }}
|
||||||
|
{{ else }}
|
||||||
|
Not started
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>End Time</th>
|
||||||
|
<td>
|
||||||
|
{{ if not .Job.TimeEnd.IsZero }}
|
||||||
|
{{ .Job.TimeEnd.Format("2006-01-02 15:04:05") }}
|
||||||
|
{{ else }}
|
||||||
|
Not completed
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Duration</th>
|
||||||
|
<td>{{ .Job.Duration }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Session Key</th>
|
||||||
|
<td>{{ .Job.SessionKey }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Section (if applicable) -->
|
||||||
|
{{ if .Job.HasError }}
|
||||||
|
<div class="card mb-4 border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5>Error</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="error-message">{{ .Job.Error }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Result Section (if completed) -->
|
||||||
|
{{ if .Job.Status == "done" }}
|
||||||
|
<div class="card mb-4 border-success">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5>Result</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="result-content">{{ .Job.Result }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Parameters Section -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Parameters</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="params-content">{{ .Job.Params }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block scripts() }}
|
||||||
|
<script>
|
||||||
|
function refreshJobDetails() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
pre.error-message {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #842029;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.result-content {
|
||||||
|
background-color: #d1e7dd;
|
||||||
|
color: #0f5132;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.params-content {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ end }}
|
224
pkg/servers/ui/views/pages/jobs.jet
Normal file
224
pkg/servers/ui/views/pages/jobs.jet
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
{{ extends "../layouts/base" }}
|
||||||
|
|
||||||
|
{{ block title() }}Job Management - HeroApp UI{{ end }}
|
||||||
|
|
||||||
|
{{ block body() }}
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">Job Management</h1>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshJobs()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Stats Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Total Jobs</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Completed Jobs</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Active Jobs</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card text-white bg-danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Error Jobs</h5>
|
||||||
|
<p class="card-text display-6">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Filters</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="filterForm" action="/jobs" method="get" class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="circle" class="form-label">Circle</label>
|
||||||
|
<select class="form-select" id="circle" name="circle" onchange="this.form.submit()">
|
||||||
|
<option value="">All Circles</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="topic" class="form-label">Topic</label>
|
||||||
|
<select class="form-select" id="topic" name="topic" onchange="this.form.submit()">
|
||||||
|
<option value="">All Topics</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="status" class="form-label">Status</label>
|
||||||
|
<select class="form-select" id="status" name="status" onchange="this.form.submit()">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="new">New</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="done">Done</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Apply Filters</button>
|
||||||
|
<a href="/jobs" class="btn btn-secondary ms-2">Clear Filters</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Tree View -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Job Tree</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="jobTree" class="job-tree">
|
||||||
|
<p>No job tree data available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job List -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Job List</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Circle</th>
|
||||||
|
<th>Topic</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Scheduled</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">No jobs available</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block scripts() }}
|
||||||
|
<script>
|
||||||
|
function toggleCircle(element) {
|
||||||
|
const topicsContainer = element.nextElementSibling;
|
||||||
|
const chevron = element.querySelector('svg');
|
||||||
|
|
||||||
|
if (topicsContainer.style.display === 'none') {
|
||||||
|
topicsContainer.style.display = 'block';
|
||||||
|
chevron.classList.add('rotate-90');
|
||||||
|
} else {
|
||||||
|
topicsContainer.style.display = 'none';
|
||||||
|
chevron.classList.remove('rotate-90');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTopic(element) {
|
||||||
|
const jobsContainer = element.nextElementSibling;
|
||||||
|
const chevron = element.querySelector('svg');
|
||||||
|
|
||||||
|
if (jobsContainer.style.display === 'none') {
|
||||||
|
jobsContainer.style.display = 'block';
|
||||||
|
chevron.classList.add('rotate-90');
|
||||||
|
} else {
|
||||||
|
jobsContainer.style.display = 'none';
|
||||||
|
chevron.classList.remove('rotate-90');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshJobs() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.job-tree {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-header, .topic-header {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-header:hover, .topic-header:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-name, .topic-name {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-node {
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-link {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-node, .topic-node {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topics-container, .jobs-container {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ end }}
|
157
pkg/servers/ui/views/pages/rpcui.jet
Normal file
157
pkg/servers/ui/views/pages/rpcui.jet
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
{{extends "../layouts/base"}}
|
||||||
|
|
||||||
|
{{block title()}}
|
||||||
|
OpenRPC UI
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{block css()}}
|
||||||
|
<link rel="stylesheet" href="/static/css/rpcui.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{block body()}}
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">OpenRPC UI</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Select OpenRPC Specification</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="specForm" action="/rpcui" method="get" class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="spec" class="form-label">Specification</label>
|
||||||
|
<select class="form-select" id="spec" name="spec" onchange="this.form.submit()">
|
||||||
|
<option value="">Select a specification</option>
|
||||||
|
{{ if SpecList }}
|
||||||
|
{{ range spec := SpecList }}
|
||||||
|
<option value="{{ spec }}" >
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<option value="" disabled>No specifications available</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="socketPath" class="form-label">Socket Path</label>
|
||||||
|
<input type="text" class="form-control" id="socketPath" name="socketPath" value="{{ SocketPath }}" placeholder="e.g., /tmp/rpc.sock">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary">Apply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p>This is the OpenRPC UI page. It allows you to interact with OpenRPC specifications.</p>
|
||||||
|
<p>Currently available specs: {{ if SpecList }}{{ len(SpecList) }}{{ else }}0{{ end }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if SelectedSpec }}
|
||||||
|
<div class="row">
|
||||||
|
<!-- Method Tree -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h5>Methods</h5></div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="method-tree list-group list-group-flush">
|
||||||
|
{{ if Methods }}
|
||||||
|
{{ range m := Methods }}
|
||||||
|
<a href="/rpcui?spec={{ SelectedSpec }}&method={{ m }}&socketPath={{ SocketPath }}"
|
||||||
|
class="list-group-item list-group-item-action method-item {{ if eq(m, SelectedMethod) }}active{{ end }}">
|
||||||
|
{{ m }}
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<div class="list-group-item">No methods available</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Method Details -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ if Method }}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>{{ Method.Name }}</h5>
|
||||||
|
{{ if Method.Description }}<p class="text-muted mb-0">{{ Method.Description }}</p>{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Parameters -->
|
||||||
|
<h6>Parameters</h6>
|
||||||
|
<table class="table table-sm schema-table">
|
||||||
|
<thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{{ if Method.Params }}
|
||||||
|
{{ range p := Method.Params }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.Name }}</td>
|
||||||
|
<td><code>{{ p.Schema.Type }}</code></td>
|
||||||
|
<td>{{ if p.Required }}<span class="schema-required">Yes</span>{{ else }}<span class="schema-optional">No</span>{{ end }}</td>
|
||||||
|
<td>{{ p.Description }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<tr><td colspan="4">No parameters</td></tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<h6 class="mt-4">Result</h6>
|
||||||
|
<table class="table table-sm schema-table">
|
||||||
|
<thead><tr><th>Name</th><th>Type</th><th>Description</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ Method.Result.Name }}</td>
|
||||||
|
<td><code>{{ Method.Result.Schema.Type }}</code></td>
|
||||||
|
<td>{{ Method.Result.Description }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Try It -->
|
||||||
|
<h6 class="mt-4">Try It</h6>
|
||||||
|
<form id="rpcForm" class="mb-3">
|
||||||
|
<input type="hidden" name="selectedMethod" value="{{ SelectedMethod }}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="paramsEditor" class="form-label">Parameters:</label>
|
||||||
|
<textarea class="form-control code-editor" id="paramsEditor" rows="10">{{ ExampleParams }}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Execute</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="resultContainer" class="result-container d-none">
|
||||||
|
<h6>Result:</h6>
|
||||||
|
<pre id="resultOutput" class="bg-light p-2 rounded"></pre>
|
||||||
|
</div>
|
||||||
|
<div id="errorContainer" class="result-container d-none">
|
||||||
|
<h6>Error:</h6>
|
||||||
|
<pre id="errorOutput" class="bg-light p-2 rounded text-danger"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ else if SelectedMethod }}
|
||||||
|
<div class="alert alert-warning">Method not found: {{ SelectedMethod }}</div>
|
||||||
|
{{ else }}
|
||||||
|
<div class="alert alert-info">Select a method from the list to view details.</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{block scripts()}}
|
||||||
|
<script src="/static/js/rpcui.js"></script>
|
||||||
|
{{end}}
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/hetznerinstall"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/hetznerinstall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -6,10 +6,10 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql/dependencies"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql/dependencies"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql/gosp"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql/gosp"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql/postgres"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql/postgres"
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql/verification"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql/verification"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants for PostgreSQL installation
|
// Constants for PostgreSQL installation
|
||||||
|
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/builders/postgresql/postgres"
|
"git.threefold.info/herocode/heroagent/pkg/system/builders/postgresql/postgres"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constants for Go stored procedure
|
// Constants for Go stored procedure
|
||||||
|
@@ -26,7 +26,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/processmanager"
|
"git.threefold.info/herocode/heroagent/pkg/system/processmanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/logger"
|
"git.threefold.info/herocode/heroagent/pkg/logger"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
)
|
)
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user