included working proxy agent for interactive client connections
This commit is contained in:
		
							parent
							
								
									362b1da1a0
								
							
						
					
					
						commit
						6a0a4e9b80
					
				
							
								
								
									
										203
									
								
								main.go
								
								
								
								
							
							
						
						
									
										203
									
								
								main.go
								
								
								
								
							| 
						 | 
				
			
			@ -17,7 +17,9 @@ import (
 | 
			
		|||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/net/html"
 | 
			
		||||
	"net/http/httputil"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	// "golang.org/x/net/html"
 | 
			
		||||
	"gontrol/src/logger"
 | 
			
		||||
	"gontrol/src/randomname"
 | 
			
		||||
	api "gontrol/src/server/api"
 | 
			
		||||
| 
						 | 
				
			
			@ -217,75 +219,164 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
	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
 | 
			
		||||
// 	}
 | 
			
		||||
// 	agentPort := r.URL.Query().Get("port")
 | 
			
		||||
// 	if agentIP == "" {
 | 
			
		||||
// 		http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
 | 
			
		||||
// 		return
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
// 	// Construct the URL to proxy to
 | 
			
		||||
// 	agentURL := "http://" + agentIP + ":" + agentPort
 | 
			
		||||
 | 
			
		||||
// 	// 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>")
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// proxyAgentHandler proxies requests to the agent server specified by IP and port query parameters.
 | 
			
		||||
// It strips the "/proxyAgent" prefix from the request path before forwarding.
 | 
			
		||||
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
 | 
			
		||||
	agentIP := r.URL.Query().Get("ip")
 | 
			
		||||
	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)
 | 
			
		||||
	agentPort := r.URL.Query().Get("port")
 | 
			
		||||
	if agentPort == "" {
 | 
			
		||||
		http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
 | 
			
		||||
		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)
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
	agentBaseURL := "http://" + agentIP + ":" + agentPort
 | 
			
		||||
 | 
			
		||||
	targetURL, err := url.Parse(agentBaseURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, "Invalid agent URL: "+err.Error(), http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	proxy := httputil.NewSingleHostReverseProxy(targetURL)
 | 
			
		||||
 | 
			
		||||
	// Modify the Director function to rewrite the request URL before sending to agent
 | 
			
		||||
	originalDirector := proxy.Director
 | 
			
		||||
	proxy.Director = func(req *http.Request) {
 | 
			
		||||
		originalDirector(req)
 | 
			
		||||
 | 
			
		||||
		// Strip "/proxyAgent" prefix from the path
 | 
			
		||||
		const prefix = "/proxyAgent"
 | 
			
		||||
		if strings.HasPrefix(req.URL.Path, prefix) {
 | 
			
		||||
			req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
 | 
			
		||||
			if req.URL.Path == "" {
 | 
			
		||||
				req.URL.Path = "/"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for c := n.FirstChild; c != nil; c = c.NextSibling {
 | 
			
		||||
			extractStylesheets(c)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Preserve original query parameters except ip and port (remove those for backend)
 | 
			
		||||
		query := req.URL.Query()
 | 
			
		||||
		query.Del("ip")
 | 
			
		||||
		query.Del("port")
 | 
			
		||||
		req.URL.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
		// Optional: set the Host header to the target host
 | 
			
		||||
		req.Host = targetURL.Host
 | 
			
		||||
	}
 | 
			
		||||
	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>")
 | 
			
		||||
	proxy.ServeHTTP(w, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
//     agentIP := r.URL.Query().Get("ip")
 | 
			
		||||
//     if agentIP == "" {
 | 
			
		||||
//         http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
 | 
			
		||||
//         return
 | 
			
		||||
//     }
 | 
			
		||||
//     agentPort := r.URL.Query().Get("port")
 | 
			
		||||
//     if agentPort == "" {
 | 
			
		||||
//         http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
 | 
			
		||||
//         return
 | 
			
		||||
//     }
 | 
			
		||||
 | 
			
		||||
//     agentURL := "http://" + agentIP + ":" + agentPort + "/"
 | 
			
		||||
 | 
			
		||||
//     resp, err := http.Get(agentURL)
 | 
			
		||||
//     if err != nil {
 | 
			
		||||
//         http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
 | 
			
		||||
//         return
 | 
			
		||||
//     }
 | 
			
		||||
//     defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
//     // Copy headers from agent response (you might want to filter some)
 | 
			
		||||
//     for k, v := range resp.Header {
 | 
			
		||||
//         for _, vv := range v {
 | 
			
		||||
//             w.Header().Add(k, vv)
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
//     w.WriteHeader(resp.StatusCode)
 | 
			
		||||
 | 
			
		||||
//     // Stream the entire response body directly to the client
 | 
			
		||||
//     io.Copy(w, resp.Body)
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
document.addEventListener("DOMContentLoaded", function() {
 | 
			
		||||
	const input = document.getElementById("command-input");
 | 
			
		||||
	const fileInput = document.getElementById("fileInput");
 | 
			
		||||
	let isUploadTriggered = false;
 | 
			
		||||
 | 
			
		||||
	document.querySelector("form").addEventListener("submit", function(event) {
 | 
			
		||||
		const command = input.value.trim();
 | 
			
		||||
		const parts = command.split(" ");
 | 
			
		||||
 | 
			
		||||
		if (parts[0] === "upload") {
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
			if (parts.length === 1) {
 | 
			
		||||
				fileInput.click();
 | 
			
		||||
				isUploadTriggered = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				const filePath = parts[1];
 | 
			
		||||
				const targetPath = parts.length > 2 ? parts[2] : ".";
 | 
			
		||||
				uploadFileFromBrowser(filePath, targetPath);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	fileInput.addEventListener("change", function () {
 | 
			
		||||
		if (fileInput.files.length > 0 && isUploadTriggered) {
 | 
			
		||||
			const file = fileInput.files[0];
 | 
			
		||||
			input.value = 'upload "' + file.name + '"';
 | 
			
		||||
			isUploadTriggered = false;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function uploadFileFromBrowser(filePath, targetPath) {
 | 
			
		||||
	const fileInput = document.getElementById("fileInput");
 | 
			
		||||
	if (fileInput.files.length === 0) {
 | 
			
		||||
		console.error("No file selected");
 | 
			
		||||
		alert("No file selected");
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const file = fileInput.files[0];
 | 
			
		||||
	const formData = new FormData();
 | 
			
		||||
	formData.append("file", file);
 | 
			
		||||
	fetch("/upload", {
 | 
			
		||||
		method: "POST",
 | 
			
		||||
		body:  formData,
 | 
			
		||||
	})
 | 
			
		||||
		.then((response) => response.text())
 | 
			
		||||
		.then((data) => {
 | 
			
		||||
			console.log("Upload successful:", data);
 | 
			
		||||
			document.getElementById("command-input").value = "";
 | 
			
		||||
		})
 | 
			
		||||
		.catch((error) => console.error("Upload failed:", error));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
/* :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 #fff;  /* optional: for visual clarity */
 | 
			
		||||
    padding: 10px;           /* optional: spacing inside the container */
 | 
			
		||||
    /* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
 | 
			
		||||
      font-family: "Lucida Console", Monaco, monospace;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log-info {
 | 
			
		||||
    color: var(--bs-success);
 | 
			
		||||
}
 | 
			
		||||
.log-warning {
 | 
			
		||||
    color: var(--bs-warning);
 | 
			
		||||
}
 | 
			
		||||
.log-error {
 | 
			
		||||
    color: var(--bs-danger);
 | 
			
		||||
}
 | 
			
		||||
.log-fatal {
 | 
			
		||||
    color: var(--bs-purple);
 | 
			
		||||
}
 | 
			
		||||
.log-debug {
 | 
			
		||||
    color: var(--bs-primary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#graph-container {
 | 
			
		||||
  margin: 0; padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr.selected-row {
 | 
			
		||||
  background-color: var(--bs-table-hover-bg) !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
const helpMsg = `
 | 
			
		||||
This is a non interactive Webshell with an interactive mode, including some
 | 
			
		||||
additional features to ease communications between server and client.
 | 
			
		||||
  Available Commands:
 | 
			
		||||
    upload            Upload files to the server through the file selector of the browser.
 | 
			
		||||
    download <file>   Download files from the server to your local download directory.
 | 
			
		||||
    theme <theme>     Change the colorscheme of the shell. Type theme to get an overview of all colorschemes.
 | 
			
		||||
    start-interactive Opens a bash shell in an interactive terminal. Type ctrl+d to go back to non-interactive mode.
 | 
			
		||||
`
 | 
			
		||||
// const helpMsg = 'This is a non interactive Webshell including some additional features to ease communications between server and client.\n  Available Commands:\n    upload\t\t\t\tUpload files to the server through the file selector of the browser.\n    download <file>\t\t\tDownload files from the server to your local download directory.\n    theme <theme>\t\t\tChange the colorscheme of the shell. Type theme to get an overview of all colorschemes.\n    start-interactive\t\t\tOpens a bash shell in an interactive terminal. Type ctrl+d to exi the interactive shell.'
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", function() {
 | 
			
		||||
	const input = document.getElementById("command-input");
 | 
			
		||||
	const terminal = document.getElementById("terminal");
 | 
			
		||||
 | 
			
		||||
	input.addEventListener("keydown", function(event) {
 | 
			
		||||
		if (event.key === "Enter") {
 | 
			
		||||
			const command = input.value.trim();
 | 
			
		||||
			if (command.startsWith("help")) {
 | 
			
		||||
				event.preventDefault();
 | 
			
		||||
				addLogEntry(helpMsg, 'info');
 | 
			
		||||
				input.value = '';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
    function addLogEntry(message, type) {
 | 
			
		||||
        const logEntry = document.createElement("div");
 | 
			
		||||
        logEntry.classList.add(type === 'error' ? 'error' : 'info');
 | 
			
		||||
        logEntry.textContent = message;
 | 
			
		||||
        terminal.appendChild(logEntry);
 | 
			
		||||
        terminal.scrollTop = terminal.scrollHeight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,132 @@
 | 
			
		|||
document.addEventListener("DOMContentLoaded", function() {
 | 
			
		||||
	const input = document.getElementById("command-input");
 | 
			
		||||
	if (!input) return;
 | 
			
		||||
	let commandHistory = JSON.parse(sessionStorage.getItem("commandHistory")) || [];
 | 
			
		||||
	let historyIndex = commandHistory.length;
 | 
			
		||||
	let tabIndex = -1;
 | 
			
		||||
	let tabMatches = [];
 | 
			
		||||
 | 
			
		||||
	document.querySelector("form").addEventListener("submit", function(event) {
 | 
			
		||||
		const command = input.value.trim();
 | 
			
		||||
		if (command) {
 | 
			
		||||
			commandHistory.push(command);
 | 
			
		||||
			sessionStorage.setItem("commandHistory", JSON.stringify(commandHistory));
 | 
			
		||||
			historyIndex = commandHistory.length;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	window.addEventListener("keydown", function(event) {
 | 
			
		||||
		if (document.activeElement !== input) return;
 | 
			
		||||
 | 
			
		||||
		const cursorPos = input.selectionStart;
 | 
			
		||||
		const textLength = input.value.length;
 | 
			
		||||
 | 
			
		||||
		// Prevent default behavior for specific Ctrl+Key shortcuts
 | 
			
		||||
		if (event.ctrlKey && ["w", "n", "p", "h", "e", "a", "k", "u", "d", "r", "t"].includes(event.key.toLowerCase())) {
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
			event.stopPropagation();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+A: Move cursor to the beginning
 | 
			
		||||
		if (event.ctrlKey && event.key === "a") {
 | 
			
		||||
			input.setSelectionRange(0, 0);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+E: Move cursor to the end
 | 
			
		||||
		else if (event.ctrlKey && event.key === "e") {
 | 
			
		||||
			input.setSelectionRange(textLength, textLength);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+U: Clear the input field
 | 
			
		||||
		else if (event.ctrlKey && event.key === "u") {
 | 
			
		||||
			input.value = "";
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+K: Delete everything after the cursor
 | 
			
		||||
		else if (event.ctrlKey && event.key === "k") {
 | 
			
		||||
			input.value = input.value.substring(0, cursorPos);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+W: Delete the previous word
 | 
			
		||||
		else if (event.ctrlKey && event.key === "w") {
 | 
			
		||||
			const beforeCursor = input.value.substring(0, cursorPos);
 | 
			
		||||
			const afterCursor = input.value.substring(cursorPos);
 | 
			
		||||
			const newBeforeCursor = beforeCursor.replace(/\S+\s*$/, ""); // Delete last word
 | 
			
		||||
			input.value = newBeforeCursor + afterCursor;
 | 
			
		||||
			input.setSelectionRange(newBeforeCursor.length, newBeforeCursor.length);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+H: Delete the previous character (Backspace)
 | 
			
		||||
		else if (event.ctrlKey && event.key === "h") {
 | 
			
		||||
			if (cursorPos > 0) {
 | 
			
		||||
				input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
 | 
			
		||||
				input.setSelectionRange(cursorPos - 1, cursorPos - 1);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+D: Delete character under cursor (or clear input if empty)
 | 
			
		||||
		else if (event.ctrlKey && event.key === "d") {
 | 
			
		||||
			if (textLength === 0) {
 | 
			
		||||
				console.log("Ctrl+D: No input, simulating EOF");
 | 
			
		||||
			} else if (cursorPos < textLength) {
 | 
			
		||||
				input.value = input.value.substring(0, cursorPos) + input.value.substring(cursorPos + 1);
 | 
			
		||||
				input.setSelectionRange(cursorPos, cursorPos);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+P: Previous command (up)
 | 
			
		||||
		else if (event.ctrlKey && event.key === "p") {
 | 
			
		||||
			if (historyIndex > 0) {
 | 
			
		||||
				historyIndex--;
 | 
			
		||||
				input.value = commandHistory[historyIndex];
 | 
			
		||||
			} else if (historyIndex === 0) {
 | 
			
		||||
				input.value = commandHistory[0];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+N: Next command (down)
 | 
			
		||||
		else if (event.ctrlKey && event.key === "n") {
 | 
			
		||||
			if (historyIndex < commandHistory.length - 1) {
 | 
			
		||||
				historyIndex++;
 | 
			
		||||
				input.value = commandHistory[historyIndex];
 | 
			
		||||
			} else {
 | 
			
		||||
				historyIndex = commandHistory.length;
 | 
			
		||||
				input.value = "";
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Ctrl+R: Prevent page reload (for future reverse search)
 | 
			
		||||
		else if (event.ctrlKey && event.key === "r") {
 | 
			
		||||
			console.log("Reverse search triggered (not yet implemented)");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Tab Completion
 | 
			
		||||
		else if (event.key === "Tab") {
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
			const currentText = input.value.trim();
 | 
			
		||||
 | 
			
		||||
			if (currentText === "") return;
 | 
			
		||||
 | 
			
		||||
			// Find all matching commands from history
 | 
			
		||||
			if (tabIndex === -1) {
 | 
			
		||||
				tabMatches = commandHistory.filter(cmd => cmd.startsWith(currentText));
 | 
			
		||||
				if (tabMatches.length === 0) return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Cycle through matches
 | 
			
		||||
			if (event.shiftKey) {
 | 
			
		||||
				tabIndex = tabIndex > 0 ? tabIndex - 1 : tabMatches.length - 1; // Shift+Tab goes backward
 | 
			
		||||
			} else {
 | 
			
		||||
				tabIndex = (tabIndex + 1) % tabMatches.length;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			input.value = tabMatches[tabIndex];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Allow Enter key to submit form
 | 
			
		||||
		if (event.key === "Enter") {
 | 
			
		||||
			tabIndex = -1; // Reset tab completion cycle
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
	}, true);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,132 @@
 | 
			
		|||
document.addEventListener("DOMContentLoaded", function () {
 | 
			
		||||
  const input = document.getElementById("command-input");
 | 
			
		||||
  // We assume your normal terminal UI is in the element with id "terminal".
 | 
			
		||||
  const normalTerminal = document.getElementById("terminal");
 | 
			
		||||
  let interactiveWS = null;
 | 
			
		||||
  let interactiveMode = false;
 | 
			
		||||
  const ansi_up = new AnsiUp;
 | 
			
		||||
 | 
			
		||||
  input.addEventListener("keydown", function (event) {
 | 
			
		||||
    if (!interactiveMode && event.key === "Enter") {
 | 
			
		||||
      const command = input.value.trim();
 | 
			
		||||
      if (command === "start-interactive") {
 | 
			
		||||
        startInteractiveSession();
 | 
			
		||||
        input.value = "";
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // Otherwise, your normal HTTP submission…
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function startInteractiveSession() {
 | 
			
		||||
  interactiveMode = true;
 | 
			
		||||
  // Hide the normal terminal and input.
 | 
			
		||||
  normalTerminal.style.display = "none";
 | 
			
		||||
  input.style.display = "none";
 | 
			
		||||
 | 
			
		||||
  // Create a new container for xterm.js.
 | 
			
		||||
  const xtermContainer = document.createElement("div");
 | 
			
		||||
  xtermContainer.id = "xterm-container";
 | 
			
		||||
  xtermContainer.style.position = "fixed";
 | 
			
		||||
  xtermContainer.style.top = "0";
 | 
			
		||||
  xtermContainer.style.left = "0";
 | 
			
		||||
  xtermContainer.style.width = window.innerWidth + "px";
 | 
			
		||||
  xtermContainer.style.height = window.innerHeight + "px";
 | 
			
		||||
  xtermContainer.style.zIndex = "1000";
 | 
			
		||||
  document.body.appendChild(xtermContainer);
 | 
			
		||||
 | 
			
		||||
  const term = new Terminal({
 | 
			
		||||
    cursorBlink: true,
 | 
			
		||||
    cursorStyle: 'block',
 | 
			
		||||
    scrollback: 1000,
 | 
			
		||||
    fontSize: 18,
 | 
			
		||||
    theme: {
 | 
			
		||||
      background: "#222",
 | 
			
		||||
      foreground: "#eee"
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  const fitAddon = new FitAddon.FitAddon();
 | 
			
		||||
  term.loadAddon(fitAddon);
 | 
			
		||||
  term.open(xtermContainer);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    fitAddon.fit();
 | 
			
		||||
    term.focus();
 | 
			
		||||
    console.log("Initial fit: container width =", xtermContainer.offsetWidth, "cols =", term.cols);
 | 
			
		||||
  }, 100);
 | 
			
		||||
 | 
			
		||||
  interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
 | 
			
		||||
  interactiveWS.binaryType = "arraybuffer";
 | 
			
		||||
 | 
			
		||||
  interactiveWS.onopen = function () {
 | 
			
		||||
    sendResize();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  interactiveWS.onmessage = function (event) {
 | 
			
		||||
    const text = new TextDecoder("utf-8").decode(event.data);
 | 
			
		||||
    term.write(text);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  interactiveWS.onclose = function () {
 | 
			
		||||
    interactiveMode = false;
 | 
			
		||||
    term.write("\r\n--- Interactive session ended ---\r\n");
 | 
			
		||||
    if (xtermContainer.parentNode) {
 | 
			
		||||
      xtermContainer.parentNode.removeChild(xtermContainer);
 | 
			
		||||
    }
 | 
			
		||||
    window.removeEventListener("resize", handleResize);
 | 
			
		||||
    normalTerminal.style.display = "block";
 | 
			
		||||
    input.style.display = "block";
 | 
			
		||||
    input.focus();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  interactiveWS.onerror = function (err) {
 | 
			
		||||
    term.write("\r\n--- Error in interactive session ---\r\n");
 | 
			
		||||
    console.error("Interactive WS error:", err);
 | 
			
		||||
    interactiveMode = false;
 | 
			
		||||
    if (xtermContainer.parentNode) {
 | 
			
		||||
      xtermContainer.parentNode.removeChild(xtermContainer);
 | 
			
		||||
    }
 | 
			
		||||
    window.removeEventListener("resize", handleResize);
 | 
			
		||||
    normalTerminal.style.display = "block";
 | 
			
		||||
    input.style.display = "block";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  term.onData(function (data) {
 | 
			
		||||
    if (data === "\x04") {
 | 
			
		||||
      term.write("\r\n--- Exiting interactive session ---\r\n");
 | 
			
		||||
      interactiveWS.close();
 | 
			
		||||
    } else {
 | 
			
		||||
      interactiveWS.send(data);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function handleResize() {
 | 
			
		||||
    const newWidth = window.innerWidth;
 | 
			
		||||
    const newHeight = window.innerHeight;
 | 
			
		||||
    xtermContainer.style.width = newWidth + "px";
 | 
			
		||||
    xtermContainer.style.height = newHeight + "px";
 | 
			
		||||
    console.log("Resizing: new width =", newWidth, "new height =", newHeight);
 | 
			
		||||
    fitAddon.fit();
 | 
			
		||||
    sendResize();
 | 
			
		||||
  }
 | 
			
		||||
  window.addEventListener("resize", handleResize);
 | 
			
		||||
 | 
			
		||||
  // Send a resize message using a custom control prefix (0xFF).
 | 
			
		||||
  function sendResize() {
 | 
			
		||||
    const resizeData = {
 | 
			
		||||
      type: "resize",
 | 
			
		||||
      cols: term.cols,
 | 
			
		||||
      rows: term.rows
 | 
			
		||||
    };
 | 
			
		||||
    const jsonStr = JSON.stringify(resizeData);
 | 
			
		||||
    const encoder = new TextEncoder();
 | 
			
		||||
    const jsonBuffer = encoder.encode(jsonStr);
 | 
			
		||||
    // Create a Uint8Array with one extra byte for the prefix.
 | 
			
		||||
    const buffer = new Uint8Array(jsonBuffer.length + 1);
 | 
			
		||||
    buffer[0] = 0xFF; // Control prefix.
 | 
			
		||||
    buffer.set(jsonBuffer, 1);
 | 
			
		||||
    interactiveWS.send(buffer.buffer);
 | 
			
		||||
    console.log("Sent resize: cols =", term.cols, "rows =", term.rows);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +1,155 @@
 | 
			
		|||
/* :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 #fff;  /* optional: for visual clarity */
 | 
			
		||||
    padding: 10px;           /* optional: spacing inside the container */
 | 
			
		||||
    /* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
 | 
			
		||||
:root {
 | 
			
		||||
	--bg-color: #222;
 | 
			
		||||
	--text-color: #eee;
 | 
			
		||||
	--command-color: #75df0b;
 | 
			
		||||
	--error-color: #ff5555;
 | 
			
		||||
	--directory-color: #1bc9e7;
 | 
			
		||||
	--ps1-color: #75df0b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
 | 
			
		||||
      font-family: "Lucida Console", Monaco, monospace;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
/* Default is pwny theme */
 | 
			
		||||
html, body {
 | 
			
		||||
	background-color: var(--bg-color);
 | 
			
		||||
	color: var(--text-color);
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
	font-size: 14pt;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
#terminal {
 | 
			
		||||
	flex: 1;
 | 
			
		||||
	padding: 12px;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	white-space: pre-wrap;
 | 
			
		||||
	border: none;
 | 
			
		||||
	margin: 10px 10px 0 10px;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	padding-bottom: 3vh;
 | 
			
		||||
}
 | 
			
		||||
#terminal form {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	/* flex-wrap: nowrap; */
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
}
 | 
			
		||||
#terminal .command {
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	/* overflow: hidden; */
 | 
			
		||||
	/* text-overflow: ellipsis; */
 | 
			
		||||
}
 | 
			
		||||
/* current-line is for interactive mode */
 | 
			
		||||
#current-line {
 | 
			
		||||
	display: inline;
 | 
			
		||||
}
 | 
			
		||||
input {
 | 
			
		||||
	flex-grow: 1;
 | 
			
		||||
	font-size: 14pt;
 | 
			
		||||
	background: var(--bg-color);
 | 
			
		||||
	color: var(--text-color);
 | 
			
		||||
	border: none;
 | 
			
		||||
	min-width: 100vh;
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
	margin-left: 8px;
 | 
			
		||||
}
 | 
			
		||||
input:focus {
 | 
			
		||||
	outline: none;
 | 
			
		||||
}
 | 
			
		||||
span.cursor {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	width: 10px;
 | 
			
		||||
	animation: blink 1s step-end infinite;
 | 
			
		||||
	vertical-align: bottom;
 | 
			
		||||
}
 | 
			
		||||
@keyframes blink {
 | 
			
		||||
	50% { opacity: 0; }
 | 
			
		||||
}
 | 
			
		||||
span.command {
 | 
			
		||||
	color: var(--command-color);
 | 
			
		||||
}
 | 
			
		||||
span.error {
 | 
			
		||||
	color: var(--error-color);
 | 
			
		||||
}
 | 
			
		||||
span.directory {
 | 
			
		||||
	color: var(--directory-color);
 | 
			
		||||
}
 | 
			
		||||
span.ps1 {
 | 
			
		||||
	color: var(--ps1-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.log-info {
 | 
			
		||||
    color: var(--bs-success);
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
	/* text-align: justify; */
 | 
			
		||||
	/* color: var(--text-color); */
 | 
			
		||||
	/* animation: 0.75s 2 changeColor; */
 | 
			
		||||
	animation: changeColor 1.75s forwards;
 | 
			
		||||
	/* max-width: 80ch; */
 | 
			
		||||
}
 | 
			
		||||
.log-warning {
 | 
			
		||||
    color: var(--bs-warning);
 | 
			
		||||
}
 | 
			
		||||
.log-error {
 | 
			
		||||
    color: var(--bs-danger);
 | 
			
		||||
}
 | 
			
		||||
.log-fatal {
 | 
			
		||||
    color: var(--bs-purple);
 | 
			
		||||
}
 | 
			
		||||
.log-debug {
 | 
			
		||||
    color: var(--bs-primary);
 | 
			
		||||
@keyframes changeColor {
 | 
			
		||||
	from { color: var(--command-color) };
 | 
			
		||||
	to { color: var(--text-color) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#graph-container {
 | 
			
		||||
  margin: 0; padding: 0;
 | 
			
		||||
.error {
 | 
			
		||||
	/* text-align: justify; */
 | 
			
		||||
	color: var(--error-color);
 | 
			
		||||
	/* max-width: 80ch; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr.selected-row {
 | 
			
		||||
  background-color: var(--bs-table-hover-bg) !important;
 | 
			
		||||
.light-theme {
 | 
			
		||||
	--bg-color: #fff;
 | 
			
		||||
	--text-color: #222;
 | 
			
		||||
	--command-color: #007700;
 | 
			
		||||
	--error-color: #cc0000;
 | 
			
		||||
	--directory-color: #0044cc;
 | 
			
		||||
	--ps1-color: #222;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.catppuccin-theme {
 | 
			
		||||
	--bg-color: #363a4f;
 | 
			
		||||
	--text-color: #cad3f5;
 | 
			
		||||
	--command-color: #a6da95;
 | 
			
		||||
	--error-color: #ed8796;
 | 
			
		||||
	--directory-color: #7dc4e4;
 | 
			
		||||
	--ps1-color: #c6a0f6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.doom1-theme {
 | 
			
		||||
	--bg-color: #21242b;
 | 
			
		||||
	--text-color: #bbc2cf;
 | 
			
		||||
	--command-color: #50fa7b;
 | 
			
		||||
	--error-color: #da8548;
 | 
			
		||||
	--directory-color: #51afef;
 | 
			
		||||
	--ps1-color: #ffa8ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gruvbox-theme {
 | 
			
		||||
	--bg-color: #282828;
 | 
			
		||||
	--text-color: #ebdbb2;
 | 
			
		||||
	--command-color: #b8bb26;
 | 
			
		||||
	--error-color: #fb4934;
 | 
			
		||||
	--directory-color: #83a598;
 | 
			
		||||
	--ps1-color: #d3869b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nord-theme {
 | 
			
		||||
	--bg-color: #2e3440;
 | 
			
		||||
	--text-color: #d8dee9;
 | 
			
		||||
	--command-color: #8fbcbb;
 | 
			
		||||
	--error-color: #bf616a;
 | 
			
		||||
	--directory-color: #81a1c1;
 | 
			
		||||
	--ps1-color: #a3be8c;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dracula-theme {
 | 
			
		||||
	--bg-color: #282a36;
 | 
			
		||||
	--text-color: #f8f8f2;
 | 
			
		||||
	--command-color: #6272a4;
 | 
			
		||||
	--error-color: #ff5555;
 | 
			
		||||
	--directory-color: #bd93f9;
 | 
			
		||||
	--ps1-color: #ff79c6;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
document.addEventListener("DOMContentLoaded", function () {
 | 
			
		||||
    const input = document.getElementById("command-input");
 | 
			
		||||
    const terminal = document.getElementById("terminal");
 | 
			
		||||
    const availableThemes = ["pwny", "light", "catppuccin", "doom1", "dracula", "gruvbox", "nord"];
 | 
			
		||||
 | 
			
		||||
    // Load the saved theme from localStorage, defaulting to "pwny"
 | 
			
		||||
    let currentTheme = localStorage.getItem("theme") || "pwny";
 | 
			
		||||
    applyTheme(currentTheme);
 | 
			
		||||
 | 
			
		||||
    input.addEventListener("keydown", function (event) {
 | 
			
		||||
        if (event.key === "Enter") {
 | 
			
		||||
            const command = input.value.trim();
 | 
			
		||||
            if (command.startsWith("theme")) {
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                const parts = command.split(" ");
 | 
			
		||||
                if (parts.length === 1) {
 | 
			
		||||
                    addLogEntry(`Available themes: ${availableThemes.join(', ')}. Current theme: ${currentTheme}`, 'info');
 | 
			
		||||
                } else {
 | 
			
		||||
                    const theme = parts[1];
 | 
			
		||||
                    applyTheme(theme);
 | 
			
		||||
                    addLogEntry(`Theme changed to: ${theme}`, 'info');
 | 
			
		||||
                }
 | 
			
		||||
                input.value = ''; // Clear input
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function applyTheme(themeName) {
 | 
			
		||||
        if (availableThemes.includes(themeName)) {
 | 
			
		||||
            document.body.className = "";  // Clear all theme classes
 | 
			
		||||
            document.body.classList.add(`${themeName}-theme`);  // Add the new theme class
 | 
			
		||||
            localStorage.setItem("theme", themeName);
 | 
			
		||||
            currentTheme = themeName;
 | 
			
		||||
        } else {
 | 
			
		||||
            addLogEntry(`Error: Theme "${themeName}" not found. Available themes: ${availableThemes.join(', ')}`, 'error');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addLogEntry(message, type) {
 | 
			
		||||
        const logEntry = document.createElement("div");
 | 
			
		||||
        logEntry.classList.add(type === 'error' ? 'error' : 'info');
 | 
			
		||||
        logEntry.textContent = message;
 | 
			
		||||
        terminal.appendChild(logEntry);
 | 
			
		||||
        terminal.scrollTop = terminal.scrollHeight;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
    <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 rel="stylesheet" type="text/css" href="static/stylesheet.css">
 | 
			
		||||
    <link rel="stylesheet" type="text/css" href="static/gontrol-stylesheet.css">
 | 
			
		||||
    <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>
 | 
			
		||||
    <!-- Include Cytoscape.js -->
 | 
			
		||||
| 
						 | 
				
			
			@ -95,6 +95,7 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- <div id="agentConnect">Future Agent Tabs</div> -->
 | 
			
		||||
 | 
			
		||||
    <!-- Offcanvas for Agent Details -->
 | 
			
		||||
    <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel" data-bs-scroll="true">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@
 | 
			
		|||
    <p>Initial Contact: {{.InitialContact}}</p>
 | 
			
		||||
    <p>Last Contact: {{.LastContact}}</p>
 | 
			
		||||
    <p>Hostname: {{.HostName}}</p>
 | 
			
		||||
    <!-- <button hx-get="/proxyAgent?ip={{.IPv4Address}}" hx-target="#agentConnect" hx-swap="innerHTML">Open</button> -->
 | 
			
		||||
    <!-- <button class="btn btn-warning" hx-get="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="#agentConnect" hx-swap="innerHTML">Proxy</button> -->
 | 
			
		||||
    <a class="btn btn-warning" href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="_blank">Proxy</a>
 | 
			
		||||
    <a href="http://{{.IPv4Address}}:{{.AddPort}}" class="btn btn-info" target="_blank">Open</a>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue