542 lines
15 KiB
Go
542 lines
15 KiB
Go
package pages
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.threefold.info/herocode/heroagent/pkg/heroagent/handlers"
|
|
"git.threefold.info/herocode/heroagent/pkg/system/stats"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/shirou/gopsutil/v3/host"
|
|
)
|
|
|
|
// UptimeProvider defines an interface for getting system uptime
|
|
type UptimeProvider interface {
|
|
GetUptime() string
|
|
}
|
|
|
|
// AdminHandler handles admin-related page routes
|
|
type AdminHandler struct {
|
|
uptimeProvider UptimeProvider
|
|
statsManager *stats.StatsManager
|
|
pmSocketPath string
|
|
pmSecret string
|
|
}
|
|
|
|
// NewAdminHandler creates a new AdminHandler
|
|
func NewAdminHandler(uptimeProvider UptimeProvider, statsManager *stats.StatsManager, pmSocketPath, pmSecret string) *AdminHandler {
|
|
// If statsManager is nil, create a new one with default settings
|
|
if statsManager == nil {
|
|
var err error
|
|
statsManager, err = stats.NewStatsManagerWithDefaults()
|
|
if err != nil {
|
|
// Log the error but continue with nil statsManager
|
|
fmt.Printf("Error creating StatsManager: %v\n", err)
|
|
}
|
|
}
|
|
|
|
return &AdminHandler{
|
|
uptimeProvider: uptimeProvider,
|
|
statsManager: statsManager,
|
|
pmSocketPath: pmSocketPath,
|
|
pmSecret: pmSecret,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers all admin page routes
|
|
func (h *AdminHandler) RegisterRoutes(app *fiber.App) {
|
|
// Admin routes
|
|
admin := app.Group("/admin")
|
|
|
|
// Dashboard
|
|
admin.Get("/", h.getDashboard)
|
|
|
|
// Create service handler with the correct socket path and secret
|
|
serviceHandler := handlers.NewServiceHandler(h.pmSocketPath, h.pmSecret)
|
|
// Services routes
|
|
admin.Get("/services", serviceHandler.GetServices)
|
|
admin.Get("/services/data", serviceHandler.GetServicesFragment)
|
|
admin.Post("/services/start", serviceHandler.StartService)
|
|
admin.Post("/services/stop", serviceHandler.StopService)
|
|
admin.Post("/services/restart", serviceHandler.RestartService)
|
|
admin.Post("/services/delete", serviceHandler.DeleteService)
|
|
admin.Get("/services/logs", serviceHandler.GetServiceLogs)
|
|
|
|
// System routes
|
|
admin.Get("/system/info", h.getSystemInfo)
|
|
admin.Get("/system/hardware-stats", h.getHardwareStats)
|
|
|
|
// Create process handler
|
|
processHandler := handlers.NewProcessHandler(h.statsManager)
|
|
admin.Get("/system/processes", processHandler.GetProcesses)
|
|
admin.Get("/system/processes-data", processHandler.GetProcessesData)
|
|
|
|
// Create log handler
|
|
// Ensure log directory exists
|
|
// Using the same shared logs path as process manager
|
|
logDir := filepath.Join(os.TempDir(), "heroagent_logs")
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
fmt.Printf("Error creating log directory: %v\n", err)
|
|
}
|
|
|
|
logHandler, err := handlers.NewLogHandler(logDir)
|
|
if err != nil {
|
|
fmt.Printf("Error creating log handler: %v\n", err)
|
|
// Fallback to old implementation if log handler creation failed
|
|
admin.Get("/system/logs", h.getSystemLogs)
|
|
admin.Get("/system/logs-test", h.getSystemLogsTest)
|
|
} else {
|
|
fmt.Printf("Log handler created successfully\n")
|
|
// Use the log handler for log routes
|
|
admin.Get("/system/logs", logHandler.GetLogs)
|
|
// Keep the fragment endpoint for backward compatibility
|
|
// but it now just redirects to the main logs endpoint
|
|
admin.Get("/system/logs-fragment", logHandler.GetLogsFragment)
|
|
admin.Get("/system/logs-test", h.getSystemLogsTest) // Keep the test logs route
|
|
|
|
// Log API endpoints
|
|
app.Get("/api/logs", logHandler.GetLogsAPI)
|
|
}
|
|
|
|
admin.Get("/system/settings", h.getSystemSettings)
|
|
|
|
// OpenRPC routes
|
|
admin.Get("/openrpc", h.getOpenRPCManager)
|
|
admin.Get("/openrpc/vfs", h.getOpenRPCVFS)
|
|
admin.Get("/openrpc/vfs/logs", h.getOpenRPCVFSLogs)
|
|
|
|
// Redirect root to admin
|
|
app.Get("/", func(c *fiber.Ctx) error {
|
|
return c.Redirect("/admin")
|
|
})
|
|
}
|
|
|
|
// getDashboard renders the admin dashboard
|
|
func (h *AdminHandler) getDashboard(c *fiber.Ctx) error {
|
|
return c.Render("admin/index", fiber.Map{
|
|
"title": "Dashboard",
|
|
})
|
|
}
|
|
|
|
// getSystemInfo renders the system info page
|
|
func (h *AdminHandler) getSystemInfo(c *fiber.Ctx) error {
|
|
// Initialize default values
|
|
cpuInfo := "Unknown"
|
|
memoryInfo := "Unknown"
|
|
diskInfo := "Unknown"
|
|
networkInfo := "Unknown"
|
|
osInfo := "Unknown"
|
|
uptimeInfo := "Unknown"
|
|
|
|
// Get hardware stats from the StatsManager
|
|
var hardwareStats map[string]interface{}
|
|
if h.statsManager != nil {
|
|
hardwareStats = h.statsManager.GetHardwareStats()
|
|
} else {
|
|
// Fallback to direct function call if StatsManager is not available
|
|
hardwareStats = stats.GetHardwareStats()
|
|
}
|
|
|
|
// Extract the formatted strings - safely handle different return types
|
|
if cpuVal, ok := hardwareStats["cpu"]; ok {
|
|
switch v := cpuVal.(type) {
|
|
case string:
|
|
cpuInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
if model, ok := v["model"].(string); ok {
|
|
usage := 0.0
|
|
if usagePercent, ok := v["usage_percent"].(float64); ok {
|
|
usage = usagePercent
|
|
}
|
|
cpuInfo = fmt.Sprintf("%s (Usage: %.1f%%)", model, usage)
|
|
}
|
|
}
|
|
}
|
|
|
|
if memVal, ok := hardwareStats["memory"]; ok {
|
|
switch v := memVal.(type) {
|
|
case string:
|
|
memoryInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
total, used := 0.0, 0.0
|
|
if totalGB, ok := v["total_gb"].(float64); ok {
|
|
total = totalGB
|
|
}
|
|
if usedGB, ok := v["used_gb"].(float64); ok {
|
|
used = usedGB
|
|
}
|
|
usedPercent := 0.0
|
|
if percent, ok := v["used_percent"].(float64); ok {
|
|
usedPercent = percent
|
|
}
|
|
memoryInfo = fmt.Sprintf("%.1f GB / %.1f GB (%.1f%% used)", used, total, usedPercent)
|
|
}
|
|
}
|
|
|
|
if diskVal, ok := hardwareStats["disk"]; ok {
|
|
switch v := diskVal.(type) {
|
|
case string:
|
|
diskInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
total, used := 0.0, 0.0
|
|
if totalGB, ok := v["total_gb"].(float64); ok {
|
|
total = totalGB
|
|
}
|
|
if usedGB, ok := v["used_gb"].(float64); ok {
|
|
used = usedGB
|
|
}
|
|
usedPercent := 0.0
|
|
if percent, ok := v["used_percent"].(float64); ok {
|
|
usedPercent = percent
|
|
}
|
|
diskInfo = fmt.Sprintf("%.1f GB / %.1f GB (%.1f%% used)", used, total, usedPercent)
|
|
}
|
|
}
|
|
|
|
if netVal, ok := hardwareStats["network"]; ok {
|
|
switch v := netVal.(type) {
|
|
case string:
|
|
networkInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
var interfaces []string
|
|
if ifaces, ok := v["interfaces"].([]interface{}); ok {
|
|
for _, iface := range ifaces {
|
|
if ifaceMap, ok := iface.(map[string]interface{}); ok {
|
|
name := ifaceMap["name"].(string)
|
|
ip := ifaceMap["ip"].(string)
|
|
interfaces = append(interfaces, fmt.Sprintf("%s: %s", name, ip))
|
|
}
|
|
}
|
|
networkInfo = strings.Join(interfaces, ", ")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get OS info
|
|
hostInfo, err := host.Info()
|
|
if err == nil {
|
|
osInfo = fmt.Sprintf("%s %s (%s)", hostInfo.Platform, hostInfo.PlatformVersion, hostInfo.KernelVersion)
|
|
}
|
|
|
|
// Get uptime
|
|
if h.uptimeProvider != nil {
|
|
uptimeInfo = h.uptimeProvider.GetUptime()
|
|
}
|
|
|
|
// Render the template with the system info
|
|
return c.Render("admin/system/info", fiber.Map{
|
|
"title": "System Information",
|
|
"cpuInfo": cpuInfo,
|
|
"memoryInfo": memoryInfo,
|
|
"diskInfo": diskInfo,
|
|
"networkInfo": networkInfo,
|
|
"osInfo": osInfo,
|
|
"uptimeInfo": uptimeInfo,
|
|
})
|
|
}
|
|
|
|
// getSystemLogs renders the system logs page
|
|
func (h *AdminHandler) getSystemLogs(c *fiber.Ctx) error {
|
|
return c.Render("admin/system/logs", fiber.Map{
|
|
"title": "System Logs",
|
|
})
|
|
}
|
|
|
|
// getSystemLogsTest renders the test logs page
|
|
func (h *AdminHandler) getSystemLogsTest(c *fiber.Ctx) error {
|
|
return c.Render("admin/system/logs_test", fiber.Map{
|
|
"title": "Test Logs",
|
|
})
|
|
}
|
|
|
|
// getSystemSettings renders the system settings page
|
|
func (h *AdminHandler) getSystemSettings(c *fiber.Ctx) error {
|
|
// Get system settings
|
|
// This is a placeholder - in a real app, you would fetch settings from a database or config file
|
|
settings := map[string]interface{}{
|
|
"logLevel": "info",
|
|
"enableDebugMode": false,
|
|
"dataDirectory": "/var/lib/heroagent",
|
|
"maxLogSize": "100MB",
|
|
}
|
|
|
|
return c.Render("admin/system/settings", fiber.Map{
|
|
"title": "System Settings",
|
|
"settings": settings,
|
|
})
|
|
}
|
|
|
|
// getHardwareStats returns only the hardware stats for Unpoly polling
|
|
func (h *AdminHandler) getHardwareStats(c *fiber.Ctx) error {
|
|
// Initialize default values
|
|
cpuInfo := "Unknown"
|
|
memoryInfo := "Unknown"
|
|
diskInfo := "Unknown"
|
|
networkInfo := "Unknown"
|
|
|
|
// Get hardware stats from the StatsManager
|
|
var hardwareStats map[string]interface{}
|
|
if h.statsManager != nil {
|
|
hardwareStats = h.statsManager.GetHardwareStats()
|
|
} else {
|
|
// Fallback to direct function call if StatsManager is not available
|
|
hardwareStats = stats.GetHardwareStats()
|
|
}
|
|
|
|
// Extract the formatted strings - safely handle different return types
|
|
if cpuVal, ok := hardwareStats["cpu"]; ok {
|
|
switch v := cpuVal.(type) {
|
|
case string:
|
|
cpuInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
if model, ok := v["model"].(string); ok {
|
|
cpuInfo = model
|
|
}
|
|
}
|
|
}
|
|
|
|
if memVal, ok := hardwareStats["memory"]; ok {
|
|
switch v := memVal.(type) {
|
|
case string:
|
|
memoryInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
total, used := 0.0, 0.0
|
|
if totalGB, ok := v["total_gb"].(float64); ok {
|
|
total = totalGB
|
|
}
|
|
if usedGB, ok := v["used_gb"].(float64); ok {
|
|
used = usedGB
|
|
}
|
|
memoryInfo = fmt.Sprintf("%.1f GB / %.1f GB", used, total)
|
|
}
|
|
}
|
|
|
|
if diskVal, ok := hardwareStats["disk"]; ok {
|
|
switch v := diskVal.(type) {
|
|
case string:
|
|
diskInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
total, used := 0.0, 0.0
|
|
if totalGB, ok := v["total_gb"].(float64); ok {
|
|
total = totalGB
|
|
}
|
|
if usedGB, ok := v["used_gb"].(float64); ok {
|
|
used = usedGB
|
|
}
|
|
diskInfo = fmt.Sprintf("%.1f GB / %.1f GB", used, total)
|
|
}
|
|
}
|
|
|
|
if netVal, ok := hardwareStats["network"]; ok {
|
|
switch v := netVal.(type) {
|
|
case string:
|
|
networkInfo = v
|
|
case map[string]interface{}:
|
|
// Format the map into a string
|
|
var interfaces []string
|
|
if ifaces, ok := v["interfaces"].([]interface{}); ok {
|
|
for _, iface := range ifaces {
|
|
if ifaceMap, ok := iface.(map[string]interface{}); ok {
|
|
name := ifaceMap["name"].(string)
|
|
ip := ifaceMap["ip"].(string)
|
|
interfaces = append(interfaces, fmt.Sprintf("%s: %s", name, ip))
|
|
}
|
|
}
|
|
networkInfo = strings.Join(interfaces, ", ")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format for display
|
|
cpuUsage := "0.0%"
|
|
memUsage := "0.0%"
|
|
diskUsage := "0.0%"
|
|
|
|
// Safely extract usage percentages
|
|
if cpuVal, ok := hardwareStats["cpu"].(map[string]interface{}); ok {
|
|
if usagePercent, ok := cpuVal["usage_percent"].(float64); ok {
|
|
cpuUsage = fmt.Sprintf("%.1f%%", usagePercent)
|
|
}
|
|
}
|
|
|
|
if memVal, ok := hardwareStats["memory"].(map[string]interface{}); ok {
|
|
if usedPercent, ok := memVal["used_percent"].(float64); ok {
|
|
memUsage = fmt.Sprintf("%.1f%%", usedPercent)
|
|
}
|
|
}
|
|
|
|
if diskVal, ok := hardwareStats["disk"].(map[string]interface{}); ok {
|
|
if usedPercent, ok := diskVal["used_percent"].(float64); ok {
|
|
diskUsage = fmt.Sprintf("%.1f%%", usedPercent)
|
|
}
|
|
}
|
|
|
|
// Render only the hardware stats fragment
|
|
return c.Render("admin/system/hardware_stats_fragment", fiber.Map{
|
|
"cpuInfo": cpuInfo,
|
|
"memoryInfo": memoryInfo,
|
|
"diskInfo": diskInfo,
|
|
"networkInfo": networkInfo,
|
|
"cpuUsage": cpuUsage,
|
|
"memUsage": memUsage,
|
|
"diskUsage": diskUsage,
|
|
})
|
|
}
|
|
|
|
// getProcesses has been moved to the handlers package
|
|
// See handlers.ProcessHandler.GetProcesses
|
|
|
|
// getOpenRPCManager renders the OpenRPC Manager view page
|
|
func (h *AdminHandler) getOpenRPCManager(c *fiber.Ctx) error {
|
|
return c.Render("admin/openrpc/index", fiber.Map{
|
|
"title": "OpenRPC Manager",
|
|
})
|
|
}
|
|
|
|
// getOpenRPCVFS renders the OpenRPC VFS view page
|
|
func (h *AdminHandler) getOpenRPCVFS(c *fiber.Ctx) error {
|
|
return c.Render("admin/openrpc/vfs", fiber.Map{
|
|
"title": "VFS OpenRPC Interface",
|
|
})
|
|
}
|
|
|
|
// getOpenRPCVFSLogs renders the OpenRPC logs content for Unpoly or direct access
|
|
func (h *AdminHandler) getOpenRPCVFSLogs(c *fiber.Ctx) error {
|
|
// Get query parameters
|
|
method := c.Query("method", "")
|
|
params := c.Query("params", "")
|
|
|
|
// Define available methods and their display names
|
|
methods := []string{
|
|
"vfs_ls",
|
|
"vfs_read",
|
|
"vfs_write",
|
|
"vfs_mkdir",
|
|
"vfs_rm",
|
|
"vfs_mv",
|
|
"vfs_cp",
|
|
"vfs_exists",
|
|
"vfs_isdir",
|
|
"vfs_isfile",
|
|
}
|
|
|
|
methodDisplayNames := map[string]string{
|
|
"vfs_ls": "List Directory",
|
|
"vfs_read": "Read File",
|
|
"vfs_write": "Write File",
|
|
"vfs_mkdir": "Create Directory",
|
|
"vfs_rm": "Remove File/Directory",
|
|
"vfs_mv": "Move/Rename",
|
|
"vfs_cp": "Copy",
|
|
"vfs_exists": "Check Exists",
|
|
"vfs_isdir": "Is Directory",
|
|
"vfs_isfile": "Is File",
|
|
}
|
|
|
|
// Generate method options HTML
|
|
methodOptions := generateMethodOptions(methods, methodDisplayNames)
|
|
|
|
// Initialize variables
|
|
var requestJSON, responseJSON, responseTime string
|
|
var hasResponse bool
|
|
|
|
// If a method is selected, make the OpenRPC call
|
|
if method != "" {
|
|
// Prepare the request
|
|
requestJSON = fmt.Sprintf(`{
|
|
"jsonrpc": "2.0",
|
|
"method": "%s",
|
|
"params": %s,
|
|
"id": 1
|
|
}`, method, params)
|
|
|
|
// In a real implementation, we would make the actual OpenRPC call here
|
|
// For now, we'll just simulate a response
|
|
|
|
// Simulate response time (would be real in production)
|
|
time.Sleep(100 * time.Millisecond)
|
|
responseTime = "100ms"
|
|
|
|
// Simulate a response based on the method
|
|
switch method {
|
|
case "vfs_ls":
|
|
responseJSON = `{
|
|
"jsonrpc": "2.0",
|
|
"result": [
|
|
{"name": "file1.txt", "size": 1024, "isDir": false, "modTime": "2023-01-01T12:00:00Z"},
|
|
{"name": "dir1", "size": 0, "isDir": true, "modTime": "2023-01-01T12:00:00Z"}
|
|
],
|
|
"id": 1
|
|
}`
|
|
case "vfs_read":
|
|
responseJSON = `{
|
|
"jsonrpc": "2.0",
|
|
"result": "File content would be here",
|
|
"id": 1
|
|
}`
|
|
default:
|
|
responseJSON = `{
|
|
"jsonrpc": "2.0",
|
|
"result": "Operation completed successfully",
|
|
"id": 1
|
|
}`
|
|
}
|
|
|
|
hasResponse = true
|
|
}
|
|
|
|
// Determine if this is an Unpoly request
|
|
isUnpoly := c.Get("X-Up-Target") != ""
|
|
|
|
// If it's an Unpoly request, render just the logs fragment
|
|
if isUnpoly {
|
|
return c.Render("admin/openrpc/vfs_logs", fiber.Map{
|
|
"methodOptions": methodOptions,
|
|
"selectedMethod": method,
|
|
"params": params,
|
|
"requestJSON": requestJSON,
|
|
"responseJSON": responseJSON,
|
|
"responseTime": responseTime,
|
|
"hasResponse": hasResponse,
|
|
})
|
|
}
|
|
|
|
// Otherwise render the full page
|
|
return c.Render("admin/openrpc/vfs_overview", fiber.Map{
|
|
"title": "VFS OpenRPC Logs",
|
|
"methodOptions": methodOptions,
|
|
"selectedMethod": method,
|
|
"params": params,
|
|
"requestJSON": requestJSON,
|
|
"responseJSON": responseJSON,
|
|
"responseTime": responseTime,
|
|
"hasResponse": hasResponse,
|
|
})
|
|
}
|
|
|
|
// generateMethodOptions generates HTML option tags for method dropdown
|
|
func generateMethodOptions(methods []string, methodDisplayNames map[string]string) string {
|
|
var options []string
|
|
for _, method := range methods {
|
|
displayName, ok := methodDisplayNames[method]
|
|
if !ok {
|
|
displayName = method
|
|
}
|
|
options = append(options, fmt.Sprintf(`<option value="%s">%s</option>`, method, displayName))
|
|
}
|
|
return strings.Join(options, "\n")
|
|
}
|
|
|
|
// Note: getProcessesData has been consolidated in the API routes file
|
|
// to avoid duplication and ensure consistent behavior
|