added graph, changed layout
This commit is contained in:
		
							parent
							
								
									97c77506c8
								
							
						
					
					
						commit
						41b0d8e355
					
				
							
								
								
									
										85
									
								
								main.go
								
								
								
								
							
							
						
						
									
										85
									
								
								main.go
								
								
								
								
							| 
						 | 
					@ -3,6 +3,7 @@ package main
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"embed"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
| 
						 | 
					@ -15,7 +16,9 @@ import (
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
 | 
						// "io"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/net/html"
 | 
				
			||||||
	"gontrol/src/logger"
 | 
						"gontrol/src/logger"
 | 
				
			||||||
	"gontrol/src/randomname"
 | 
						"gontrol/src/randomname"
 | 
				
			||||||
	api "gontrol/src/server/api"
 | 
						api "gontrol/src/server/api"
 | 
				
			||||||
| 
						 | 
					@ -33,6 +36,9 @@ var (
 | 
				
			||||||
	db 	 *sql.DB
 | 
						db 	 *sql.DB
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//go:embed static/*
 | 
				
			||||||
 | 
					var staticFiles embed.FS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
| 
						 | 
					@ -127,7 +133,7 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if strings.Contains(r.Header.Get("Accept"), "application") {
 | 
						if strings.Contains(r.Header.Get("Accept"), "json") {
 | 
				
			||||||
		w.Header().Set("Content-Type", "application/json")
 | 
							w.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
		jsonData, err := json.Marshal(agents)
 | 
							jsonData, err := json.Marshal(agents)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -174,12 +180,12 @@ func getAgentIds(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
					func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	// Warning this bit me in the nose: var count is []string, but
 | 
						// Warning this bit me in the nose: var countStr is []string, but
 | 
				
			||||||
	// variable = count[0] is casted to int automatically
 | 
						// variable = countStr[0] is casted to int automatically
 | 
				
			||||||
	// when the string is a number. Jesus Christ, this is odd behavior!
 | 
						// when the string is a number. Jesus Christ, this is odd behavior!
 | 
				
			||||||
	levels := r.URL.Query()["level"]
 | 
						levels := r.URL.Query()["level"]
 | 
				
			||||||
	countStr := r.URL.Query()["limit"]
 | 
						countStr := r.URL.Query()["limit"]
 | 
				
			||||||
	var limit int = 10
 | 
						var limit int = 128
 | 
				
			||||||
	if len(countStr) > 0 {
 | 
						if len(countStr) > 0 {
 | 
				
			||||||
		parsedCount, err := strconv.Atoi(countStr[0])
 | 
							parsedCount, err := strconv.Atoi(countStr[0])
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
| 
						 | 
					@ -209,6 +215,75 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	renderTemplate(w, "templates/partials/logs_partial.html", logs)
 | 
						renderTemplate(w, "templates/partials/logs_partial.html", logs)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
 | 
				
			||||||
 | 
						if agentIP == "" {
 | 
				
			||||||
 | 
							http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Construct the URL to proxy to
 | 
				
			||||||
 | 
						agentURL := "http://" + agentIP + ":8080"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send request to agent server
 | 
				
			||||||
 | 
						resp, err := http.Get(agentURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse the HTML from the agent's response
 | 
				
			||||||
 | 
						doc, err := html.Parse(resp.Body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							http.Error(w, "Failed to parse agent response: "+err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Extract all <link> elements for stylesheets
 | 
				
			||||||
 | 
						var stylesheets []string
 | 
				
			||||||
 | 
						var extractStylesheets func(*html.Node)
 | 
				
			||||||
 | 
						extractStylesheets = func(n *html.Node) {
 | 
				
			||||||
 | 
							if n.Type == html.ElementNode && n.Data == "link" {
 | 
				
			||||||
 | 
								for _, attr := range n.Attr {
 | 
				
			||||||
 | 
									if attr.Key == "rel" && attr.Val == "stylesheet" {
 | 
				
			||||||
 | 
										for _, attr := range n.Attr {
 | 
				
			||||||
 | 
											if attr.Key == "href" {
 | 
				
			||||||
 | 
												stylesheets = append(stylesheets, attr.Val)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for c := n.FirstChild; c != nil; c = c.NextSibling {
 | 
				
			||||||
 | 
								extractStylesheets(c)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						extractStylesheets(doc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return the HTML and inject the stylesheets in the <head> section
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "text/html")
 | 
				
			||||||
 | 
						w.WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Inject the stylesheets into the <head> section of your page
 | 
				
			||||||
 | 
						fmt.Fprintf(w, "<html><head>")
 | 
				
			||||||
 | 
						for _, stylesheet := range stylesheets {
 | 
				
			||||||
 | 
							// Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
 | 
				
			||||||
 | 
							fmt.Fprintf(w, `<link rel="stylesheet" href="%s">`, stylesheet)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Fprintf(w, "</head><body>")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Now, serve the HTML content of the agent web app (or an iframe)
 | 
				
			||||||
 | 
						// Output the rest of the HTML (including the agent's content inside iframe)
 | 
				
			||||||
 | 
						fmt.Fprintf(w, `
 | 
				
			||||||
 | 
							<iframe src="%s" width="100%" height="800px" style="border:none;">
 | 
				
			||||||
 | 
								Your browser does not support iframes.
 | 
				
			||||||
 | 
							</iframe>
 | 
				
			||||||
 | 
						`, agentURL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Fprintf(w, "</body></html>")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,6 +308,8 @@ func main() {
 | 
				
			||||||
	webMux.HandleFunc("/agents/{agentId}", agentsHandler)
 | 
						webMux.HandleFunc("/agents/{agentId}", agentsHandler)
 | 
				
			||||||
	webMux.HandleFunc("/logs", logsHandler)
 | 
						webMux.HandleFunc("/logs", logsHandler)
 | 
				
			||||||
	webMux.HandleFunc("/logs/{level}", logsHandler)
 | 
						webMux.HandleFunc("/logs/{level}", logsHandler)
 | 
				
			||||||
 | 
					    webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
 | 
				
			||||||
 | 
						webMux.HandleFunc("/proxyAgent", proxyAgentHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
 | 
						db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
 | 
				
			||||||
	defer db.Close()
 | 
						defer db.Close()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,9 +107,9 @@ func GetAgents(db *sql.DB) ([]Agent, error) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetAgent(db *sql.DB, w http.ResponseWriter, r *http.Request, agentId string) (Agent, error) {
 | 
					func GetAgent(db *sql.DB, w http.ResponseWriter, r *http.Request, agentId string) (Agent, error) {
 | 
				
			||||||
	query := "Select agentId, agentName, agentType, initialContact, lastContact from agents where agentId = ?"
 | 
						query := "Select agentId, agentName, agentType, IPv4Address, initialContact, lastContact from agents where agentId = ?"
 | 
				
			||||||
	var agent Agent
 | 
						var agent Agent
 | 
				
			||||||
	err := db.QueryRow(query, agentId).Scan(&agent.AgentID, &agent.AgentName, &agent.AgentType,  &agent.InitialContact, &agent.LastContact)
 | 
						err := db.QueryRow(query, agentId).Scan(&agent.AgentID, &agent.AgentName, &agent.AgentType,&agent.IPv4Address,  &agent.InitialContact, &agent.LastContact)
 | 
				
			||||||
	if err == sql.ErrNoRows {
 | 
						if err == sql.ErrNoRows {
 | 
				
			||||||
		http.Error(w, "Agent not found", http.StatusNotFound)
 | 
							http.Error(w, "Agent not found", http.StatusNotFound)
 | 
				
			||||||
		return Agent{} , err
 | 
							return Agent{} , err
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -221,7 +221,7 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
 | 
				
			||||||
			break
 | 
								break
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		log.Printf("Message from agent %s: %s", agentName, message)
 | 
							log.Printf("Message from agent %s: %s", agentName, message)
 | 
				
			||||||
		logger.InsertLog(logger.Info, fmt.Sprintf("Message from agent %s: %s", agentName, message))
 | 
							logger.InsertLog(logger.Debug, fmt.Sprintf("Message from agent %s: %s", agentName, message))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ch, ok := responseChannels.Load(agentName); ok {
 | 
							if ch, ok := responseChannels.Load(agentName); ok {
 | 
				
			||||||
			responseChan := ch.(chan string)
 | 
								responseChan := ch.(chan string)
 | 
				
			||||||
| 
						 | 
					@ -235,67 +235,6 @@ type Message struct {
 | 
				
			||||||
	Payload string `json:"payload"`
 | 
						Payload string `json:"payload"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
 | 
					 | 
				
			||||||
// 	err := r.ParseForm()
 | 
					 | 
				
			||||||
// 	if err != nil {
 | 
					 | 
				
			||||||
// 		http.Error(w, "Invalid form data", http.StatusBadRequest)
 | 
					 | 
				
			||||||
// 		logger.InsertLog(logger.Info, "Invalid form data")
 | 
					 | 
				
			||||||
// 		return
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	agentName := r.FormValue("agentName")
 | 
					 | 
				
			||||||
// 	command := r.FormValue("command")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	agentSocketsMutex.Lock()
 | 
					 | 
				
			||||||
// 	conn, ok := agentSockets[agentName]
 | 
					 | 
				
			||||||
// 	agentSocketsMutex.Unlock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	if !ok {
 | 
					 | 
				
			||||||
// 		http.Error(w, "Agent not connected", http.StatusNotFound)
 | 
					 | 
				
			||||||
// 		logger.InsertLog(logger.Info, "Agent not connected")
 | 
					 | 
				
			||||||
// 		return
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	responseChan := make(chan string, 1)
 | 
					 | 
				
			||||||
// 	responseChannels.Store(agentName, responseChan)
 | 
					 | 
				
			||||||
// 	defer responseChannels.Delete(agentName)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	message := Message {
 | 
					 | 
				
			||||||
// 		Type: "command",
 | 
					 | 
				
			||||||
// 		Payload: command,
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	messageBytes, _ := json.Marshal(message)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	err = conn.WriteMessage(websocket.TextMessage, messageBytes)
 | 
					 | 
				
			||||||
// 	if err != nil {
 | 
					 | 
				
			||||||
// 		http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
 | 
					 | 
				
			||||||
// 		logger.InsertLog(logger.Error, "Failed to send command to the agent")
 | 
					 | 
				
			||||||
// 		return
 | 
					 | 
				
			||||||
// 	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 	select {
 | 
					 | 
				
			||||||
// 	case response := <-responseChan:
 | 
					 | 
				
			||||||
// 		var parsedResponse map[string]string
 | 
					 | 
				
			||||||
// 		if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
 | 
					 | 
				
			||||||
// 			http.Error(w, "Failed to parse response", http.StatusInternalServerError)
 | 
					 | 
				
			||||||
// 			return
 | 
					 | 
				
			||||||
// 		}
 | 
					 | 
				
			||||||
// 		payload, ok := parsedResponse["payload"]
 | 
					 | 
				
			||||||
// 		if !ok {
 | 
					 | 
				
			||||||
// 			http.Error(w, "Invalid response structure", http.StatusInternalServerError)
 | 
					 | 
				
			||||||
// 			logger.InsertLog(logger.Error, "Invalid response structure")
 | 
					 | 
				
			||||||
// 			return
 | 
					 | 
				
			||||||
// 		}
 | 
					 | 
				
			||||||
// 		w.WriteHeader(http.StatusOK)
 | 
					 | 
				
			||||||
// 		w.Header().Set("Content-Type", "text/plain")
 | 
					 | 
				
			||||||
// 		w.Write([]byte(payload))
 | 
					 | 
				
			||||||
// 	case <- time.After(10 * time.Second):
 | 
					 | 
				
			||||||
// 			http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
 | 
					 | 
				
			||||||
// 			logger.InsertLog(logger.Info, "Agent response timed out")
 | 
					 | 
				
			||||||
// 		}
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
 | 
					var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
 | 
				
			||||||
	err := r.ParseForm()
 | 
						err := r.ParseForm()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,185 @@
 | 
				
			||||||
 | 
					let agentData = [];
 | 
				
			||||||
 | 
					let isCyInitialized = false;
 | 
				
			||||||
 | 
					let cy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function initializeCytoscape() {
 | 
				
			||||||
 | 
					    if (isCyInitialized) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cy = cytoscape({
 | 
				
			||||||
 | 
					        container: document.getElementById('cyto-graph'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        style: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                selector: 'node',
 | 
				
			||||||
 | 
					                style: {
 | 
				
			||||||
 | 
					                    'background-color': '#007bff',
 | 
				
			||||||
 | 
					                    'label': 'data(name)', // Ensure the label uses the AgentName
 | 
				
			||||||
 | 
					                    'color': 'white',
 | 
				
			||||||
 | 
					                    'text-outline-width': 2,
 | 
				
			||||||
 | 
					                    'text-outline-color': '#333',
 | 
				
			||||||
 | 
					                    'width': '50px',
 | 
				
			||||||
 | 
					                    'height': '50px'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                selector: 'edge',
 | 
				
			||||||
 | 
					                style: {
 | 
				
			||||||
 | 
					                    'width': 3,
 | 
				
			||||||
 | 
					                    'line-color': '#ccc',
 | 
				
			||||||
 | 
					                    'target-arrow-color': '#ccc',
 | 
				
			||||||
 | 
					                    'target-arrow-shape': 'triangle'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        layout: {
 | 
				
			||||||
 | 
					            name: 'grid',
 | 
				
			||||||
 | 
					            rows: 2
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isCyInitialized = true;  // Mark Cytoscape as initialized
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load the graph after the page has fully loaded
 | 
				
			||||||
 | 
					document.addEventListener('DOMContentLoaded', function () {
 | 
				
			||||||
 | 
					    console.log('DOMContentLoaded fired.');
 | 
				
			||||||
 | 
					    initializeCytoscape();
 | 
				
			||||||
 | 
					    loadGraphData();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load the graph after HTMX swap
 | 
				
			||||||
 | 
					document.body.addEventListener('htmx:afterSwap', function (event) {
 | 
				
			||||||
 | 
					    console.log('htmx:afterSwap fired.');
 | 
				
			||||||
 | 
					    if (event.target.id === 'agentList') {
 | 
				
			||||||
 | 
					        initializeCytoscape();
 | 
				
			||||||
 | 
					        loadGraphData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function updateGraph(agentData) {
 | 
				
			||||||
 | 
					    if (!cy) {
 | 
				
			||||||
 | 
					        console.error('Cytoscape is not initialized yet.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('Updating graph with agent data:', agentData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Clear existing nodes and edges
 | 
				
			||||||
 | 
					    cy.elements().remove();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add nodes for each agent with the AgentName as the label
 | 
				
			||||||
 | 
					    agentData.forEach(agent => {
 | 
				
			||||||
 | 
					        const id = agent.agentId;
 | 
				
			||||||
 | 
					        const name = agent.agentName;
 | 
				
			||||||
 | 
					        const status = agent.status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (id && name) {
 | 
				
			||||||
 | 
					            let nodeColor = (status === 'Connected') ? '#28a745' : '#dc3545'; // Green for connected, Red for disconnected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cy.add({
 | 
				
			||||||
 | 
					                group: 'nodes',
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    id: id,
 | 
				
			||||||
 | 
					                    name: name,
 | 
				
			||||||
 | 
					                    status: status,
 | 
				
			||||||
 | 
					                    type: agent.agentType,
 | 
				
			||||||
 | 
					                    ip: agent.IPv4Address
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                style: {
 | 
				
			||||||
 | 
					                    'background-color': nodeColor,
 | 
				
			||||||
 | 
					                    'label': name,  // Display agent's name
 | 
				
			||||||
 | 
					                    'color': 'white',
 | 
				
			||||||
 | 
					                    'text-outline-width': 2,
 | 
				
			||||||
 | 
					                    'text-outline-color': '#333',
 | 
				
			||||||
 | 
					                    'width': '50px',
 | 
				
			||||||
 | 
					                    'height': '50px'
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            console.warn('Skipping agent with missing data:', agent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Define the target node (`g2` in your case)
 | 
				
			||||||
 | 
					    const targetNode = 'g2';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Ensure the target node (`g2`) exists, if not, create it
 | 
				
			||||||
 | 
					    if (cy.getElementById(targetNode).length === 0) {
 | 
				
			||||||
 | 
					        cy.add({
 | 
				
			||||||
 | 
					            group: 'nodes',
 | 
				
			||||||
 | 
					            data: {
 | 
				
			||||||
 | 
					                id: targetNode,
 | 
				
			||||||
 | 
					                name: 'g2',
 | 
				
			||||||
 | 
					                status: 'Target',
 | 
				
			||||||
 | 
					                type: 'Server',
 | 
				
			||||||
 | 
					                ip: 'N/A'
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            style: {
 | 
				
			||||||
 | 
					                'background-color': '#6c757d', // Gray for target node
 | 
				
			||||||
 | 
					                'label': 'g2',
 | 
				
			||||||
 | 
					                'color': 'white',
 | 
				
			||||||
 | 
					                'text-outline-width': 2,
 | 
				
			||||||
 | 
					                'text-outline-color': '#333',
 | 
				
			||||||
 | 
					                'width': '50px',
 | 
				
			||||||
 | 
					                'height': '50px'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Connect each agent to the target node (`g2`)
 | 
				
			||||||
 | 
					    agentData.forEach(agent => {
 | 
				
			||||||
 | 
					        const id = agent.agentId;
 | 
				
			||||||
 | 
					        if (id) {
 | 
				
			||||||
 | 
					            cy.add({
 | 
				
			||||||
 | 
					                group: 'edges',
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    source: id,
 | 
				
			||||||
 | 
					                    target: targetNode
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            console.warn('Skipping edge for agent with missing agentId:', agent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Force a layout update
 | 
				
			||||||
 | 
					    cy.layout({
 | 
				
			||||||
 | 
					        name: 'grid',
 | 
				
			||||||
 | 
					        rows: 2
 | 
				
			||||||
 | 
					    }).run();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetchData() {
 | 
				
			||||||
 | 
					    const url = "http://localhost:3333/agents";
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const response = await fetch(url, {headers: {Accept: 'application/json'}});
 | 
				
			||||||
 | 
					        if (!response.ok) {
 | 
				
			||||||
 | 
					            throw new Error(`Response status: ${response.status}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					        return data; // Return the fetched data
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.error(error.message);
 | 
				
			||||||
 | 
					        return []; // Return an empty array on error
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Function to get agent data and update the graph
 | 
				
			||||||
 | 
					async function loadGraphData() {
 | 
				
			||||||
 | 
					    console.log("Function loadGraphData()");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fetch agent data asynchronously
 | 
				
			||||||
 | 
					    agentData = await fetchData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if the data is valid
 | 
				
			||||||
 | 
					    console.log('Extracted agent data:', agentData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only update the graph if agent data is available
 | 
				
			||||||
 | 
					    if (agentData && agentData.length > 0) {
 | 
				
			||||||
 | 
					        await updateGraph(agentData);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        console.log('No agent data found or extracted.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					const checkboxState = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener('DOMContentLoaded', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.body.addEventListener('htmx:beforeSwap', function(event) {
 | 
				
			||||||
 | 
					        if (event.target.id === "agentList") {
 | 
				
			||||||
 | 
					            saveCheckboxState();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.body.addEventListener('htmx:afterSwap', function(event) {
 | 
				
			||||||
 | 
					        if (event.target.id === "agentList") {
 | 
				
			||||||
 | 
					            restoreCheckboxState();
 | 
				
			||||||
 | 
					            updateAgentDropdown();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function prepareAgentNames(event) {
 | 
				
			||||||
 | 
					    const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
 | 
				
			||||||
 | 
					        .filter(cb => cb.checked)
 | 
				
			||||||
 | 
					        .map(cb => cb.dataset.agentName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hiddenInput = document.getElementById('agentNamesInput');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (selected.length > 0) {
 | 
				
			||||||
 | 
					        document.getElementById('agentName').removeAttribute('name');
 | 
				
			||||||
 | 
					        hiddenInput.value = selected.join(',');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        document.getElementById('agentName').setAttribute('name', 'agentName');
 | 
				
			||||||
 | 
					        hiddenInput.value = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toggleAllCheckboxes() {
 | 
				
			||||||
 | 
					    const checkboxes = document.querySelectorAll('input[name="agent-checkbox"]');
 | 
				
			||||||
 | 
					    const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    checkboxes.forEach(checkbox => checkbox.checked = !allChecked);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function saveCheckboxState() {
 | 
				
			||||||
 | 
					    document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
 | 
				
			||||||
 | 
					        checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function restoreCheckboxState() {
 | 
				
			||||||
 | 
					    document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
 | 
				
			||||||
 | 
					        const state = checkboxState.get(checkbox.dataset.agentName);
 | 
				
			||||||
 | 
					        if (state !== undefined) {
 | 
				
			||||||
 | 
					            checkbox.checked = state;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateAgentDropdown() {
 | 
				
			||||||
 | 
					    const select = document.getElementById("agentName");
 | 
				
			||||||
 | 
					    const optionValues = Array.from(select.options).map(opt => opt.value);
 | 
				
			||||||
 | 
					    const rows = document.querySelectorAll("#agentList tbody tr");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rows.forEach(row => {
 | 
				
			||||||
 | 
					        const status = row.cells[4].textContent.trim();
 | 
				
			||||||
 | 
					        const name = row.cells[1].textContent.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (status === "Connected") {
 | 
				
			||||||
 | 
					            row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
 | 
				
			||||||
 | 
					            const option = document.createElement("option");
 | 
				
			||||||
 | 
					            if (!(optionValues.includes(name))) {
 | 
				
			||||||
 | 
					                option.value = name;
 | 
				
			||||||
 | 
					                option.textContent = name;
 | 
				
			||||||
 | 
					                select.appendChild(option);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (status === "Disconnected") {
 | 
				
			||||||
 | 
					            row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
 | 
				
			||||||
 | 
					            const option = Array.from(select.options).find(opt => opt.value === name);
 | 
				
			||||||
 | 
					            if(option) {
 | 
				
			||||||
 | 
					                select.removeChild(option);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					:root{
 | 
				
			||||||
 | 
					  --grey-color: #1B2B34;
 | 
				
			||||||
 | 
					  --error-color: #EC5f67;
 | 
				
			||||||
 | 
					  --warning-color: #F99157;
 | 
				
			||||||
 | 
					  --yellow-color: #FAC863;
 | 
				
			||||||
 | 
					  --info-color: #99C794;
 | 
				
			||||||
 | 
					  --teal-color: #5FB3B3;
 | 
				
			||||||
 | 
					  --blue-color: #6699CC;
 | 
				
			||||||
 | 
					  --debug-color: #C594C5;
 | 
				
			||||||
 | 
					  --fatal-color: #AB7967;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#logs-container {
 | 
				
			||||||
 | 
					    height: 300px;           /* or any fixed height you prefer */
 | 
				
			||||||
 | 
					    overflow-y: auto;        /* enables vertical scroll when content overflows */
 | 
				
			||||||
 | 
					    border: 1px solid #ccc;  /* optional: for visual clarity */
 | 
				
			||||||
 | 
					    padding: 10px;           /* optional: spacing inside the container */
 | 
				
			||||||
 | 
					    background-color: #f8f9fa; /* optional: subtle background for log readability */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
 | 
				
			||||||
 | 
					      font-family: "Lucida Console", Monaco, monospace;
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.log-info {
 | 
				
			||||||
 | 
					    color: var(--info-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.log-warning {
 | 
				
			||||||
 | 
					    color: var(--warning-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.log-error {
 | 
				
			||||||
 | 
					    color: var(--error-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.log-fatal {
 | 
				
			||||||
 | 
					    color: var(--fatal-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.log-debug {
 | 
				
			||||||
 | 
					    color: var(--debug-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#graph-container {
 | 
				
			||||||
 | 
					  margin: 0; padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,144 +5,83 @@
 | 
				
			||||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
					    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
 | 
					    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
 | 
				
			||||||
 | 
					    <link rel="stylesheet" type="text/css" href="static/stylesheet.css">
 | 
				
			||||||
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
 | 
					    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
 | 
				
			||||||
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
 | 
					    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
 | 
				
			||||||
    <!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> -->
 | 
					    <!-- Include Cytoscape.js -->
 | 
				
			||||||
 | 
					    <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.23/dist/cytoscape.min.js"></script>
 | 
				
			||||||
 | 
					    <script type="text/javascript" src="static/agents-graph.js"></script>
 | 
				
			||||||
 | 
					    <script type="text/javascript" src="static/gontrol-helper.js"></script>
 | 
				
			||||||
    <title>g2: gommand & gontrol</title>
 | 
					    <title>g2: gommand & gontrol</title>
 | 
				
			||||||
    <script>
 | 
					 | 
				
			||||||
    const checkboxState = new Map();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    document.addEventListener('DOMContentLoaded', () => {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        document.body.addEventListener('htmx:beforeSwap', function(event) {
 | 
					 | 
				
			||||||
            if (event.target.id === "agentList") {
 | 
					 | 
				
			||||||
                saveCheckboxState();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        document.body.addEventListener('htmx:afterSwap', function(event) {
 | 
					 | 
				
			||||||
            if (event.target.id === "agentList") {
 | 
					 | 
				
			||||||
                restoreCheckboxState();
 | 
					 | 
				
			||||||
                updateAgentDropdown();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function prepareAgentNames(event) {
 | 
					 | 
				
			||||||
        const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
 | 
					 | 
				
			||||||
            .filter(cb => cb.checked)
 | 
					 | 
				
			||||||
            .map(cb => cb.dataset.agentName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const hiddenInput = document.getElementById('agentNamesInput');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (selected.length > 0) {
 | 
					 | 
				
			||||||
            document.getElementById('agentName').removeAttribute('name');
 | 
					 | 
				
			||||||
            hiddenInput.value = selected.join(',');
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            document.getElementById('agentName').setAttribute('name', 'agentName');
 | 
					 | 
				
			||||||
            hiddenInput.value = '';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function saveCheckboxState() {
 | 
					 | 
				
			||||||
        document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
 | 
					 | 
				
			||||||
            checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function restoreCheckboxState() {
 | 
					 | 
				
			||||||
        document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
 | 
					 | 
				
			||||||
            const state = checkboxState.get(checkbox.dataset.agentName);
 | 
					 | 
				
			||||||
            if (state !== undefined) {
 | 
					 | 
				
			||||||
                checkbox.checked = state;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function updateAgentDropdown() {
 | 
					 | 
				
			||||||
        const select = document.getElementById("agentName");
 | 
					 | 
				
			||||||
        const optionValues = Array.from(select.options).map(opt => opt.value);
 | 
					 | 
				
			||||||
        const rows = document.querySelectorAll("#agentList tbody tr");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        rows.forEach(row => {
 | 
					 | 
				
			||||||
            const status = row.cells[4].textContent.trim();
 | 
					 | 
				
			||||||
            const name = row.cells[1].textContent.trim();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (status === "Connected") {
 | 
					 | 
				
			||||||
                row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
 | 
					 | 
				
			||||||
                const option = document.createElement("option");
 | 
					 | 
				
			||||||
                if (!(optionValues.includes(name))) {
 | 
					 | 
				
			||||||
                    option.value = name;
 | 
					 | 
				
			||||||
                    option.textContent = name;
 | 
					 | 
				
			||||||
                    select.appendChild(option);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (status === "Disconnected") {
 | 
					 | 
				
			||||||
                row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
 | 
					 | 
				
			||||||
                const option = Array.from(select.options).find(opt => opt.value === name);
 | 
					 | 
				
			||||||
                if(option) {
 | 
					 | 
				
			||||||
                    select.removeChild(option);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    </script>
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        :root{
 | 
					 | 
				
			||||||
          --grey-color: #1B2B34;
 | 
					 | 
				
			||||||
          --error-color: #EC5f67;
 | 
					 | 
				
			||||||
          --warning-color: #F99157;
 | 
					 | 
				
			||||||
          --yellow-color: #FAC863;
 | 
					 | 
				
			||||||
          --info-color: #99C794;
 | 
					 | 
				
			||||||
          --teal-color: #5FB3B3;
 | 
					 | 
				
			||||||
          --blue-color: #6699CC;
 | 
					 | 
				
			||||||
          --debug-color: #C594C5;
 | 
					 | 
				
			||||||
          --fatal-color: #AB7967;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .log-info, .log-warning, .log-error, .log-fatal, .log-debug{
 | 
					 | 
				
			||||||
              font-family: "Lucida Console", Monaco, monospace;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .log-info {
 | 
					 | 
				
			||||||
            color: var(--info-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .log-warning {
 | 
					 | 
				
			||||||
            color: var(--warning-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .log-error {
 | 
					 | 
				
			||||||
            color: var(--error-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .log-fatal {
 | 
					 | 
				
			||||||
            color: var(--fatal-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .log-debug {
 | 
					 | 
				
			||||||
            color: var(--debug-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
    <div class="container">
 | 
					  <div class="container-fluid px-4 py-3">
 | 
				
			||||||
        <div class="row">
 | 
					 | 
				
			||||||
            <div class="col">
 | 
					 | 
				
			||||||
                <h2>Agents</h2>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Top Row: Graph + Logs (equal height) -->
 | 
				
			||||||
 | 
					    <div class="row mb-3">
 | 
				
			||||||
 | 
					      <!-- Graph -->
 | 
				
			||||||
 | 
					      <div class="col-md-6 d-flex flex-column">
 | 
				
			||||||
 | 
					        <h3>Agents</h3>
 | 
				
			||||||
 | 
					        <div class="flex-grow-1 border" id="cyto-graph" style="height: 100%; min-height: 320px;"></div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- Logs -->
 | 
				
			||||||
 | 
					      <div class="col-md-6 d-flex flex-column">
 | 
				
			||||||
 | 
					        <h3>Logs</h3>
 | 
				
			||||||
 | 
					        <form id="log-filter-form"
 | 
				
			||||||
 | 
					              hx-get="/logs"
 | 
				
			||||||
 | 
					              hx-target="#logs-container"
 | 
				
			||||||
 | 
					              hx-swap="innerHTML"
 | 
				
			||||||
 | 
					              hx-trigger="change from:.log-filter, load, every 5s"
 | 
				
			||||||
 | 
					              hx-include="#log-filter-form">
 | 
				
			||||||
 | 
					          <div class="btn-group mb-2" role="group" aria-label="Log filter">
 | 
				
			||||||
 | 
					            <input type="checkbox" class="btn-check log-filter" id="info" name="level" value="info" checked>
 | 
				
			||||||
 | 
					            <label class="btn btn-outline-primary" for="info">Info</label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <input type="checkbox" class="btn-check log-filter" id="warning" name="level" value="warning">
 | 
				
			||||||
 | 
					            <label class="btn btn-outline-primary" for="warning">Warning</label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <input type="checkbox" class="btn-check log-filter" id="error" name="level" value="error">
 | 
				
			||||||
 | 
					            <label class="btn btn-outline-primary" for="error">Error</label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <input type="checkbox" class="btn-check log-filter" id="fatal" name="level" value="fatal">
 | 
				
			||||||
 | 
					            <label class="btn btn-outline-primary" for="fatal">Fatal</label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <input type="checkbox" class="btn-check log-filter" id="debug" name="level" value="debug">
 | 
				
			||||||
 | 
					            <label class="btn btn-outline-primary" for="debug">Debug</label>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					        <div id="logs-container" class="flex-grow-1 border" style="overflow-y: auto; min-height: 320px;">
 | 
				
			||||||
 | 
					          <!-- Logs will load here -->
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Bottom Row: Agent List + Command Execution -->
 | 
				
			||||||
 | 
					    <div class="row">
 | 
				
			||||||
      <!-- Agent List -->
 | 
					      <!-- Agent List -->
 | 
				
			||||||
                <div id="agentList" hx-get="/agents" hx-trigger="load, every 2s" hx-swap="innerHTML"></div>
 | 
					      <div class="col-md-6">
 | 
				
			||||||
                <!-- <div id="agentList" hx-get="/agents" hx-trigger="load" hx-swap="innerHTML"></div> -->
 | 
					        <div id="agentList"
 | 
				
			||||||
                <!-- Agent Commands -->
 | 
					             hx-get="/agents"
 | 
				
			||||||
 | 
					             hx-trigger="load, every 2s"
 | 
				
			||||||
 | 
					             hx-swap="innerHTML">
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- Command Execution -->
 | 
				
			||||||
 | 
					      <div class="col-md-6">
 | 
				
			||||||
        <div id="agentCommands">
 | 
					        <div id="agentCommands">
 | 
				
			||||||
                    <h3>Command Execution</h3>
 | 
					          <h5>Command Execution</h5>
 | 
				
			||||||
                    <form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded"  hx-swap="innerHTML" onsubmit="prepareAgentNames(event)">
 | 
					          <form hx-post="http://localhost:5555/executeCommand"
 | 
				
			||||||
 | 
					                hx-target="#commandOutput"
 | 
				
			||||||
 | 
					                hx-encoding="application/x-www-form-urlencoded"
 | 
				
			||||||
 | 
					                hx-swap="innerHTML"
 | 
				
			||||||
 | 
					                onsubmit="prepareAgentNames(event)">
 | 
				
			||||||
            <div class="mb-3">
 | 
					            <div class="mb-3">
 | 
				
			||||||
              <label for="agentName" class="form-label">Agent Name</label>
 | 
					              <label for="agentName" class="form-label">Agent Name</label>
 | 
				
			||||||
                            <!-- <select class="form-select" id="agentName" name="agentName" required> -->
 | 
					              <select id="agentName" class="form-select" name="agentName"
 | 
				
			||||||
                            <select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown">
 | 
					                      hx-on="htmx:afterSwap:updateAgentDropdown">
 | 
				
			||||||
                <option value="" disabled selected>Select an Agent</option>
 | 
					                <option value="" disabled selected>Select an Agent</option>
 | 
				
			||||||
                                <!-- Dynamically populated with agent names -->
 | 
					 | 
				
			||||||
              </select>
 | 
					              </select>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="mb-3">
 | 
					            <div class="mb-3">
 | 
				
			||||||
| 
						 | 
					@ -150,40 +89,24 @@
 | 
				
			||||||
              <input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
 | 
					              <input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <button type="submit" class="btn btn-primary">Execute</button>
 | 
					            <button type="submit" class="btn btn-primary">Execute</button>
 | 
				
			||||||
                    <!-- Hidden checkbox form !-->
 | 
					 | 
				
			||||||
            <input type="hidden" name="agentNames" id="agentNamesInput">
 | 
					            <input type="hidden" name="agentNames" id="agentNamesInput">
 | 
				
			||||||
          </form>
 | 
					          </form>
 | 
				
			||||||
          <pre id="commandOutput"></pre>
 | 
					          <pre id="commandOutput"></pre>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
        <!-- Agent Details -->
 | 
					 | 
				
			||||||
        <div class="col" id="agentDetails">
 | 
					 | 
				
			||||||
            <h3>Details</h3>
 | 
					 | 
				
			||||||
            <p>Select an agent to view details.</p>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <!-- Logs Section -->
 | 
					    <!-- Offcanvas for Agent Details -->
 | 
				
			||||||
        <h3>Logs</h3>
 | 
					    <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel" data-bs-scroll="true">
 | 
				
			||||||
 | 
					      <div class="offcanvas-header">
 | 
				
			||||||
            <form id="log-filter-form"
 | 
					        <h5 class="offcanvas-title" id="offcanvasRightLabel">Agent Details</h5>
 | 
				
			||||||
              hx-get="/logs"
 | 
					        <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
 | 
				
			||||||
              hx-target="#logs-container"
 | 
					      </div>
 | 
				
			||||||
              hx-swap="innerHTML"
 | 
					      <div id="offcanvas-body" class="offcanvas-body">
 | 
				
			||||||
              hx-trigger="change from:.log-filter, every 3s"
 | 
					        ...
 | 
				
			||||||
              hx-include="#log-filter-form">
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              <label><input type="checkbox" class="log-filter" name="level" value="info" checked> Info</label>
 | 
					 | 
				
			||||||
              <label><input type="checkbox" class="log-filter" name="level" value="warning"> Warning</label>
 | 
					 | 
				
			||||||
              <label><input type="checkbox" class="log-filter" name="level" value="error"> Error</label>
 | 
					 | 
				
			||||||
              <label><input type="checkbox" class="log-filter" name="level" value="debug"> Debug</label>
 | 
					 | 
				
			||||||
              <label><input type="checkbox" class="log-filter" name="level" value="fatal"> Fatal</label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div id="logs-container">
 | 
					 | 
				
			||||||
                <!-- Logs will load here -->
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logs.forEarch(log => {
 | 
					            logs.forEarch(log => {
 | 
				
			||||||
                const logElement = document.createElement('p');
 | 
					                const logElement = document.createElement('p');
 | 
				
			||||||
                logElement.innerHTML = `<strong>ts=${log.timestamp} level=${log.level}</strong> msg=${log.message}`;
 | 
					                logElement.innerHTML = `<strong>level=${log.level}</strong> msg=${log.message}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (log.level ==== 'INFO') {
 | 
					            if (log.level ==== 'INFO') {
 | 
				
			||||||
                logElement.classList.add('log-info');
 | 
					                logElement.classList.add('log-info');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,11 @@
 | 
				
			||||||
<div id="agent-detail">
 | 
					<div id="agent-detail">
 | 
				
			||||||
    <h2>Agent Details</h2>
 | 
					    <!-- <h2>Agent Details</h2> -->
 | 
				
			||||||
    <p>ID: {{.AgentID}}</p>
 | 
					    <p>ID: {{.AgentID}}</p>
 | 
				
			||||||
    <p>Name: {{.AgentName}}</p>
 | 
					    <p>Name: {{.AgentName}}</p>
 | 
				
			||||||
    <p>Type: {{.AgentType}}</p>
 | 
					    <p>Type: {{.AgentType}}</p>
 | 
				
			||||||
 | 
					    <p>IP Addr: {{.IPv4Address}}</p>
 | 
				
			||||||
    <p>Initial Contact: {{.InitialContact}}</p>
 | 
					    <p>Initial Contact: {{.InitialContact}}</p>
 | 
				
			||||||
    <p>Last Contact: {{.LastContact}}</p>
 | 
					    <p>Last Contact: {{.LastContact}}</p>
 | 
				
			||||||
    <button hx-get="/agents" hx-target="#agentList" hx-swap="innerHTML">Back to List</button>
 | 
					    <!-- <button hx-get="/proxyAgent?ip={{.IPv4Address}}" hx-target="#agentConnect" hx-swap="innerHTML">Open</button> -->
 | 
				
			||||||
 | 
					    <a href="http://{{.IPv4Address}}:8080" class="btn btn-info" target="_blank">Open</a>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@
 | 
				
			||||||
            <!-- <th>Initial Contact</th> -->
 | 
					            <!-- <th>Initial Contact</th> -->
 | 
				
			||||||
            <!-- <th>Last Contact</th> -->
 | 
					            <!-- <th>Last Contact</th> -->
 | 
				
			||||||
            <th>Status</th>
 | 
					            <th>Status</th>
 | 
				
			||||||
            <th>Actions</th>
 | 
					            <!-- <th>Actions <input type="checkbox" class="select-agent-checkbox" onClick="toggleAllCheckboxes(this)"></th> -->
 | 
				
			||||||
 | 
					            <th><button type="button" class="btn btn-primary btn-sm" onClick="toggleAllCheckboxes()">Toggle Agents</button></th>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
    </thead>
 | 
					    </thead>
 | 
				
			||||||
    <tbody>
 | 
					    <tbody>
 | 
				
			||||||
| 
						 | 
					@ -24,10 +25,14 @@
 | 
				
			||||||
            <!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
 | 
					            <!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
 | 
				
			||||||
            <td>{{.Status}}</td>
 | 
					            <td>{{.Status}}</td>
 | 
				
			||||||
            <td>
 | 
					            <td>
 | 
				
			||||||
                <input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}">
 | 
					                <button class="btn btn-danger btn-sm" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
 | 
				
			||||||
                <button class="btn btn-warning" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button>
 | 
					                <!-- <button class="btn btn-warning btn-sm" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <button class="btn btn-danger" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
 | 
					                <button class="btn btn-warning btn-sm" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight" hx-get="/agents/{{.AgentID}}" hx-target="#offcanvas-body">View</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}" name="agent-checkbox">
 | 
				
			||||||
 | 
					                <!-- <input type="checkbox" class="agent-checkbox btn-check" data-agent-name="{{.AgentName}}" id="{{.AgentName}}"> -->
 | 
				
			||||||
 | 
					                <!-- <label class="btn btn-outline-primary btn-sm" for="{{.AgentName}}">Select</label> -->
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        {{end}}
 | 
					        {{end}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
{{range .}}
 | 
					{{range .}}
 | 
				
			||||||
    <div class="log-{{.Level}}">
 | 
					    <div class="log-{{.Level}}">
 | 
				
			||||||
            ts={{.Timestamp}} level={{.Level}}msg="{{.Message}}"
 | 
					            ts={{.Timestamp}} level={{.Level}} msg="{{.Message}}"
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
{{end}}
 | 
					{{end}}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue