From 3f01074e3fc4585884bd599422bcf35ad0dea4e2 Mon Sep 17 00:00:00 2001
From: despiegk
` tag): + ```html + + ``` + * Include local custom CSS (e.g., `/static/css/custom.css`). + * Include local custom JS (e.g., `/static/js/custom.js`). + * Structure: + * Navbar (include `components/navbar.jet`) + * Main container (Bootstrap `container-fluid` or similar) + * Sidebar (include `components/sidebar.jet`) + * Content area (where page-specific content will be injected) +* **Navbar (`views/components/navbar.jet`):** + * Bootstrap Navbar component. + * Site title/logo. + * Login/Logout buttons (stubs, basic links for now). +* **Sidebar/Tree Menu (`views/components/sidebar.jet`):** + * Bootstrap navigation component (e.g., Navs, List group). + * Links: + * Dashboard + * Process Manager + +--- + +## 3. Pages + +* **Dashboard Page:** + * **Controller (`controllers/dashboard_controller.go`):** + * `ShowDashboard()`: Renders the dashboard page. + * **View (`views/pages/dashboard.jet`):** + * Extends `layouts/base.jet`. + * Simple placeholder content for now (e.g., "Welcome to the Dashboard!"). +* **Process Manager Page:** + * **Model (`models/process_model.go`):** + * `GetProcesses()`: Function to call `pkg/system/processmanager` to get a list of running processes. Will need to define a struct for process information (PID, Name, CPU, Memory). + * `KillProcess(pid string)`: Function to call `pkg/system/processmanager` to terminate a process. + * **Controller (`controllers/process_controller.go`):** + * `ShowProcessManager()`: + * Calls `models.GetProcesses()`. + * Passes process data to the view. + * Renders `views/pages/process_manager.jet`. + * `HandleKillProcess()`: (Handles POST request to kill a process) + * Extracts PID from request. + * Calls `models.KillProcess(pid)`. + * Redirects back to the process manager page or returns a status. + * **View (`views/pages/process_manager.jet`):** + * Extends `layouts/base.jet`. + * Displays processes in a Bootstrap table: + * Columns: PID, Name, CPU, Memory, Actions. + * Actions column: "Kill" button for each process (linking to `HandleKillProcess` or using JS for an AJAX call). + +--- + +## 4. Fiber & Jet Integration (`app.go` and `routes/router.go`) + +* **`pkg/servers/ui/app.go`:** + * `NewApp()` function: + * Initialize Fiber app: `fiber.New()`. + * Initialize Jet template engine: + * `jet.NewSet(jet.NewOSFileSystemLoader("./pkg/servers/ui/views"), jet.InDevelopmentMode())` (or adjust path as needed). + * Pass Jet views to Fiber: `app.Settings.Views = views`. + * Setup static file serving: `app.Static("/static", "./pkg/servers/ui/static")`. + * Setup routes: Call a function from `routes/router.go`. + * Return the Fiber app instance. + * This `app.go` can then be imported and run from your main application entry point (e.g., in `cmd/heroagent/main.go`). +* **`pkg/servers/ui/routes/router.go`:** + * `SetupRoutes(app *fiber.App, processController *controllers.ProcessController, ...)` function: + * `app.Get("/", dashboardController.ShowDashboard)` + * `app.Get("/processes", processController.ShowProcessManager)` + * `app.Post("/processes/kill/:pid", processController.HandleKillProcess)` (or similar for kill action) + * `app.Get("/login", authController.ShowLoginPage)` (stub) + * `app.Post("/login", authController.HandleLogin)` (stub) + * `app.Get("/logout", authController.HandleLogout)` (stub) + +--- + +## 5. Authentication Stubs + +* **`controllers/auth_controller.go`:** + * `ShowLoginPage()`: Renders a simple login form. + * `HandleLogin()`: Placeholder logic (e.g., always "logs in" or checks a hardcoded credential). Sets a dummy session/cookie. + * `HandleLogout()`: Placeholder logic. Clears dummy session/cookie. +* **`views/pages/login.jet`:** + * Simple Bootstrap form for username/password. + +--- + +## 6. Dependencies (go.mod) + +Ensure these are added to your `go.mod` file: +* `github.com/gofiber/fiber/v2` +* `github.com/CloudyKit/jet/v6` + +--- + +## Request Flow Example: Process Manager Page + +```mermaid +sequenceDiagram + participant User + participant Browser + participant FiberApp [Fiber App (pkg/servers/ui/app.go)] + participant Router [routes/router.go] + participant ProcessCtrl [controllers/process_controller.go] + participant ProcessMdl [models/process_model.go] + participant SysProcMgr [pkg/system/processmanager] + participant JetEngine [CloudyKit/jet/v6] + participant View [views/pages/process_manager.jet] + + User->>Browser: Navigates to /processes + Browser->>FiberApp: GET /processes + FiberApp->>Router: Route request + Router->>ProcessCtrl: Calls ShowProcessManager() + ProcessCtrl->>ProcessMdl: Calls GetProcesses() + ProcessMdl->>SysProcMgr: Fetches process list + SysProcMgr-->>ProcessMdl: Returns process data + ProcessMdl-->>ProcessCtrl: Returns process data + ProcessCtrl->>JetEngine: Renders process_manager.jet with data + JetEngine->>View: Populates template + View-->>JetEngine: Rendered HTML + JetEngine-->>ProcessCtrl: Rendered HTML + ProcessCtrl-->>FiberApp: Returns HTML response + FiberApp-->>Browser: Sends HTML response + Browser->>User: Displays Process Manager page \ No newline at end of file diff --git a/pkg/servers/ui/app.go b/pkg/servers/ui/app.go new file mode 100644 index 0000000..0c6564b --- /dev/null +++ b/pkg/servers/ui/app.go @@ -0,0 +1,43 @@ +package ui + +import ( + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui/routes" // Import the routes package + "github.com/gofiber/fiber/v2" + jetadapter "github.com/gofiber/template/jet/v2" // Aliased for clarity +) + +// AppConfig holds the configuration for the UI application. +type AppConfig struct { + // Any specific configurations can be added here later +} + +// NewApp creates and configures a new Fiber application for the UI. +func NewApp(config AppConfig) *fiber.App { + // Initialize Jet template engine + // Using OSFileSystemLoader to load templates from the filesystem. + // The path is relative to where the application is run. + // For development, InDevelopmentMode can be true to reload templates on each request. + engine := jetadapter.New("./pkg/servers/ui/views", ".jet") + + // Enable template reloading for development. + // Set to false or remove this line for production. + engine.Reload(true) + + // If you need to add custom functions or global variables to Jet: + // engine.AddFunc("myCustomFunc", func(arg jet.Arguments) reflect.Value { ... }) + // engine.AddGlobal("myGlobalVar", "someValue") + + // Create a new Fiber app with the configured Jet engine + app := fiber.New(fiber.Config{ + Views: engine, + }) + + // Setup static file serving + // Files in ./pkg/servers/ui/static will be accessible via /static URL path + app.Static("/static", "./pkg/servers/ui/static") + + // Setup routes + routes.SetupRoutes(app) + + return app +} diff --git a/pkg/servers/ui/controllers/.gitkeep b/pkg/servers/ui/controllers/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/controllers/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/controllers/auth_controller.go b/pkg/servers/ui/controllers/auth_controller.go new file mode 100644 index 0000000..1f26879 --- /dev/null +++ b/pkg/servers/ui/controllers/auth_controller.go @@ -0,0 +1,71 @@ +package controllers + +import "github.com/gofiber/fiber/v2" + +// AuthController handles authentication-related requests. +type AuthController struct { + // Add dependencies like a user service or session manager here +} + +// NewAuthController creates a new instance of AuthController. +func NewAuthController() *AuthController { + return &AuthController{} +} + +// ShowLoginPage renders the login page. +// @Summary Show login page +// @Description Displays the user login form. +// @Tags auth +// @Produce html +// @Success 200 {string} html "Login page HTML" +// @Router /login [get] +func (ac *AuthController) ShowLoginPage(c *fiber.Ctx) error { + return c.Render("pages/login", fiber.Map{ + "Title": "Login", + }) +} + +// HandleLogin processes the login form submission. +// @Summary Process user login +// @Description Authenticates the user based on submitted credentials. +// @Tags auth +// @Accept x-www-form-urlencoded +// @Produce json +// @Param username formData string true "Username" +// @Param password formData string true "Password" +// @Success 302 "Redirects to dashboard on successful login" +// @Failure 400 {object} fiber.Map "Error for invalid input" +// @Failure 401 {object} fiber.Map "Error for authentication failure" +// @Router /login [post] +func (ac *AuthController) HandleLogin(c *fiber.Ctx) error { + // username := c.FormValue("username") + // password := c.FormValue("password") + + // TODO: Implement actual authentication logic here. + // For now, we'll just simulate a successful login and redirect. + // In a real app, you would: + // 1. Validate username and password. + // 2. Check credentials against a user store (e.g., database). + // 3. Create a session or token. + + // Simulate successful login + // c.Cookie(&fiber.Cookie{Name: "session_token", Value: "dummy_token", HttpOnly: true, SameSite: "Lax"}) + return c.Redirect("/") // Redirect to dashboard +} + +// HandleLogout processes the logout request. +// @Summary Process user logout +// @Description Logs the user out by clearing their session. +// @Tags auth +// @Success 302 "Redirects to login page" +// @Router /logout [get] +func (ac *AuthController) HandleLogout(c *fiber.Ctx) error { + // TODO: Implement actual logout logic here. + // For now, we'll just simulate a logout and redirect. + // In a real app, you would: + // 1. Invalidate the session or token. + // 2. Clear any session-related cookies. + + // c.ClearCookie("session_token") + return c.Redirect("/login") +} diff --git a/pkg/servers/ui/controllers/dashboard_controller.go b/pkg/servers/ui/controllers/dashboard_controller.go new file mode 100644 index 0000000..ac65ae7 --- /dev/null +++ b/pkg/servers/ui/controllers/dashboard_controller.go @@ -0,0 +1,28 @@ +package controllers + +import "github.com/gofiber/fiber/v2" + +// DashboardController handles requests related to the dashboard. +type DashboardController struct { + // Add any dependencies here, e.g., a service to fetch dashboard data +} + +// NewDashboardController creates a new instance of DashboardController. +func NewDashboardController() *DashboardController { + return &DashboardController{} +} + +// ShowDashboard renders the main dashboard page. +// @Summary Show the main dashboard +// @Description Displays the main dashboard page with an overview. +// @Tags dashboard +// @Produce html +// @Success 200 {string} html "Dashboard page HTML" +// @Router / [get] +func (dc *DashboardController) ShowDashboard(c *fiber.Ctx) error { + // For now, just render the dashboard template. + // Later, you might pass data to the template. + return c.Render("pages/dashboard", fiber.Map{ + "Title": "Dashboard", // This can be used in base.jet {{ .Title }} + }) +} diff --git a/pkg/servers/ui/controllers/process_controller.go b/pkg/servers/ui/controllers/process_controller.go new file mode 100644 index 0000000..895c612 --- /dev/null +++ b/pkg/servers/ui/controllers/process_controller.go @@ -0,0 +1,76 @@ +package controllers + +import ( + "fmt" + "strconv" + + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui/models" + "github.com/gofiber/fiber/v2" +) + +// ProcessController handles requests related to process management. +type ProcessController struct { + ProcessService models.ProcessManagerService +} + +// NewProcessController creates a new instance of ProcessController. +func NewProcessController(ps models.ProcessManagerService) *ProcessController { + return &ProcessController{ + ProcessService: ps, + } +} + +// ShowProcessManager renders the process manager page. +// @Summary Show process manager +// @Description Displays a list of running processes. +// @Tags process +// @Produce html +// @Success 200 {string} html "Process manager page HTML" +// @Failure 500 {object} fiber.Map "Error message if processes cannot be fetched" +// @Router /processes [get] +func (pc *ProcessController) ShowProcessManager(c *fiber.Ctx) error { + processes, err := pc.ProcessService.GetProcesses() + if err != nil { + // Log the error appropriately in a real application + fmt.Println("Error fetching processes:", err) + return c.Status(fiber.StatusInternalServerError).Render("pages/process_manager", fiber.Map{ + "Title": "Process Manager", + "Processes": []models.Process{}, // Empty list on error + "Error": "Failed to retrieve process list.", + }) + } + + return c.Render("pages/process_manager", fiber.Map{ + "Title": "Process Manager", + "Processes": processes, + }) +} + +// HandleKillProcess handles the request to kill a specific process. +// @Summary Kill a process +// @Description Terminates a process by its PID. +// @Tags process +// @Produce html +// @Param pid path int true "Process ID" +// @Success 302 "Redirects to process manager page" +// @Failure 400 {object} fiber.Map "Error message if PID is invalid" +// @Failure 500 {object} fiber.Map "Error message if process cannot be killed" +// @Router /processes/kill/{pid} [post] +func (pc *ProcessController) HandleKillProcess(c *fiber.Ctx) error { + pidStr := c.Params("pid") + pid, err := strconv.Atoi(pidStr) + if err != nil { + // Log error + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid PID format"}) + } + + err = pc.ProcessService.KillProcess(pid) + if err != nil { + // Log error + // In a real app, you might want to return a more user-friendly error page or message + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to kill process"}) + } + + // Redirect back to the process manager page + return c.Redirect("/processes") +} diff --git a/pkg/servers/ui/models/.gitkeep b/pkg/servers/ui/models/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/models/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/models/process_model.go b/pkg/servers/ui/models/process_model.go new file mode 100644 index 0000000..17200d6 --- /dev/null +++ b/pkg/servers/ui/models/process_model.go @@ -0,0 +1,48 @@ +package models + +// Process represents a single running process with its relevant details. +type Process struct { + PID int `json:"pid"` + Name string `json:"name"` + CPU float64 `json:"cpu"` // CPU usage percentage + Memory float64 `json:"memory"` // Memory usage in MB + // Add other fields if needed, e.g., User, Status, Path +} + +// ProcessManagerService defines the interface for interacting with the system's process manager. +// This will be implemented by a struct that calls pkg/system/processmanager. +type ProcessManagerService interface { + GetProcesses() ([]Process, error) + KillProcess(pid int) error +} + +// TODO: Implement a concrete ProcessManagerService that uses pkg/system/processmanager. +// For now, we can create a mock implementation for development and testing of the UI. + +// MockProcessManager is a mock implementation of ProcessManagerService for UI development. +type MockProcessManager struct{} + +// GetProcesses returns a list of mock processes. +func (m *MockProcessManager) GetProcesses() ([]Process, error) { + // Return some mock data + return []Process{ + {PID: 1001, Name: "SystemIdleProcess", CPU: 95.5, Memory: 0.1}, + {PID: 1002, Name: "explorer.exe", CPU: 1.2, Memory: 150.7}, + {PID: 1003, Name: "chrome.exe", CPU: 25.8, Memory: 512.3}, + {PID: 1004, Name: "code.exe", CPU: 5.1, Memory: 350.0}, + {PID: 1005, Name: "go.exe", CPU: 0.5, Memory: 80.2}, + }, nil +} + +// KillProcess simulates killing a process. +func (m *MockProcessManager) KillProcess(pid int) error { + // In a real implementation, this would call the system process manager. + // For mock, we just print a message or do nothing. + // fmt.Printf("Mock: Attempting to kill process %d\n", pid) + return nil +} + +// NewMockProcessManager creates a new instance of MockProcessManager. +func NewMockProcessManager() ProcessManagerService { + return &MockProcessManager{} +} diff --git a/pkg/servers/ui/routes/.gitkeep b/pkg/servers/ui/routes/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/routes/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/routes/router.go b/pkg/servers/ui/routes/router.go new file mode 100644 index 0000000..7db7a60 --- /dev/null +++ b/pkg/servers/ui/routes/router.go @@ -0,0 +1,50 @@ +package routes + +import ( + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui/controllers" + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui/models" + "github.com/gofiber/fiber/v2" +) + +// SetupRoutes configures the application's routes. +func SetupRoutes(app *fiber.App) { + // Initialize services and controllers + // For now, using the mock process manager + processManagerService := models.NewMockProcessManager() + + dashboardController := controllers.NewDashboardController() + processController := controllers.NewProcessController(processManagerService) + authController := controllers.NewAuthController() + + // --- Public Routes --- + // Login and Logout + app.Get("/login", authController.ShowLoginPage) + app.Post("/login", authController.HandleLogin) + app.Get("/logout", authController.HandleLogout) + + // --- Authenticated Routes --- + // TODO: Add middleware here to protect routes that require authentication. + // For example: + // authenticated := app.Group("/", authMiddleware) // Assuming authMiddleware is defined + // authenticated.Get("/", dashboardController.ShowDashboard) + // authenticated.Get("/processes", processController.ShowProcessManager) + // authenticated.Post("/processes/kill/:pid", processController.HandleKillProcess) + + // For now, routes are public for development ease + app.Get("/", dashboardController.ShowDashboard) + app.Get("/processes", processController.ShowProcessManager) + app.Post("/processes/kill/:pid", processController.HandleKillProcess) + +} + +// TODO: Implement authMiddleware +// func authMiddleware(c *fiber.Ctx) error { +// // Check for session/token +// // If not authenticated, redirect to /login +// // If authenticated, c.Next() +// // Example: +// // if c.Cookies("session_token") == "" { +// // return c.Redirect("/login") +// // } +// return c.Next() +// } diff --git a/pkg/servers/ui/static/css/.gitkeep b/pkg/servers/ui/static/css/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/static/css/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/static/css/custom.css b/pkg/servers/ui/static/css/custom.css new file mode 100644 index 0000000..e77ea2a --- /dev/null +++ b/pkg/servers/ui/static/css/custom.css @@ -0,0 +1,48 @@ +/* Custom CSS for HeroApp UI */ + +body { + /* Example: Add some padding if needed, beyond what Bootstrap provides */ + /* padding-top: 5rem; */ +} + +.sidebar { + position: fixed; + top: 0; + /* Sidenav can be customized further */ + bottom: 0; + left: 0; + z-index: 100; /* Behind the navbar */ + padding: 56px 0 0; /* Height of navbar */ + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); +} + +@media (max-width: 767.98px) { + .sidebar { + top: 5rem; /* Adjust if navbar height changes */ + padding: 0; + } +} + +.sidebar .nav-link { + font-weight: 500; + color: #333; +} + +.sidebar .nav-link .feather { + margin-right: 4px; + color: #727272; +} + +.sidebar .nav-link.active { + color: #007bff; +} + +.sidebar .nav-link:hover .feather, +.sidebar .nav-link.active .feather { + color: inherit; +} + +.sidebar-heading { + font-size: .75rem; + text-transform: uppercase; +} \ No newline at end of file diff --git a/pkg/servers/ui/static/img/.gitkeep b/pkg/servers/ui/static/img/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/static/img/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/static/js/.gitkeep b/pkg/servers/ui/static/js/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/static/js/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/static/js/custom.js b/pkg/servers/ui/static/js/custom.js new file mode 100644 index 0000000..643dc14 --- /dev/null +++ b/pkg/servers/ui/static/js/custom.js @@ -0,0 +1,15 @@ +// Custom JavaScript for HeroApp UI + +document.addEventListener('DOMContentLoaded', function () { + console.log('HeroApp UI custom.js loaded'); + + // Example: Add a click listener to a button with ID 'myButton' + // const myButton = document.getElementById('myButton'); + // if (myButton) { + // myButton.addEventListener('click', function() { + // alert('Button clicked!'); + // }); + // } + + // You can add more specific JavaScript interactions here as needed. +}); \ No newline at end of file diff --git a/pkg/servers/ui/views/components/.gitkeep b/pkg/servers/ui/views/components/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/views/components/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/views/components/navbar.jet b/pkg/servers/ui/views/components/navbar.jet new file mode 100644 index 0000000..ff76056 --- /dev/null +++ b/pkg/servers/ui/views/components/navbar.jet @@ -0,0 +1,24 @@ +{{ block navbar() }} + + +
+{{ end }} \ No newline at end of file diff --git a/pkg/servers/ui/views/components/sidebar.jet b/pkg/servers/ui/views/components/sidebar.jet new file mode 100644 index 0000000..c3aae15 --- /dev/null +++ b/pkg/servers/ui/views/components/sidebar.jet @@ -0,0 +1,17 @@ +{{ block sidebar() }} +
+{{ end }} \ No newline at end of file diff --git a/pkg/servers/ui/views/layouts/.gitkeep b/pkg/servers/ui/views/layouts/.gitkeep new file mode 100644 index 0000000..23b09d8 --- /dev/null +++ b/pkg/servers/ui/views/layouts/.gitkeep @@ -0,0 +1 @@ +# This file is intentionally left blank to ensure the directory is tracked by Git. \ No newline at end of file diff --git a/pkg/servers/ui/views/layouts/base.jet b/pkg/servers/ui/views/layouts/base.jet new file mode 100644 index 0000000..dfe4457 --- /dev/null +++ b/pkg/servers/ui/views/layouts/base.jet @@ -0,0 +1,37 @@ + + +
+ + +
+ + + + + +
+