cleanup
This commit is contained in:
		
							parent
							
								
									45568013ca
								
							
						
					
					
						commit
						2b0f897f18
					
				| 
						 | 
				
			
			@ -14,6 +14,10 @@ import (
 | 
			
		|||
	"slices"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"io"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"gontrol/src/logger"
 | 
			
		||||
	"gontrol/src/server/api"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -173,60 +177,240 @@ func (a *App) getAgentIds(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
 | 
			
		||||
// selected by ?ip=…&port=… .  It keeps the path *after* /proxyAgent,
 | 
			
		||||
// removes the two query parameters, disables HTTP/2 (mandatory for WS),
 | 
			
		||||
// and streams with no buffering.
 | 
			
		||||
func (a *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
        ip   := r.URL.Query().Get("ip")
 | 
			
		||||
        port := r.URL.Query().Get("port")
 | 
			
		||||
        if ip == "" || port == "" {
 | 
			
		||||
                http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
 | 
			
		||||
                return
 | 
			
		||||
        }
 | 
			
		||||
// func readCookie(r *http.Request, name string) (string, bool){
 | 
			
		||||
// 	c, err := r.Cookie(name)
 | 
			
		||||
// 	if err != nil { return "", false }
 | 
			
		||||
// 	v, _ := url.QueryUnescape(c.Value)
 | 
			
		||||
// 	return v, true
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
        // We leave the scheme "http" even for WebSockets – the Upgrade
 | 
			
		||||
        // header does the rest.  (Only cosmetic to change it to "ws”.)
 | 
			
		||||
        target, err := url.Parse("http://" + ip + ":" + port)
 | 
			
		||||
        if err != nil {
 | 
			
		||||
                http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
 | 
			
		||||
                return
 | 
			
		||||
        }
 | 
			
		||||
// func setCookie(h http.Header, name, val string) {
 | 
			
		||||
// 	h.Add("Set-Cookie", (&http.Cookie{
 | 
			
		||||
// 		Name: 		name,
 | 
			
		||||
// 		Value:      url.QueryEscape(val),
 | 
			
		||||
// 		Path:       "/proxyAgent",
 | 
			
		||||
// 		SameSite:   http.SameSiteLaxMode,
 | 
			
		||||
// 	}).String())
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
        proxy := &httputil.ReverseProxy{
 | 
			
		||||
                Director: func(req *http.Request) {
 | 
			
		||||
                        // Point to the agent
 | 
			
		||||
                        req.URL.Scheme = target.Scheme
 | 
			
		||||
                        req.URL.Host   = target.Host
 | 
			
		||||
// // proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
 | 
			
		||||
// // selected by ?ip=…&port=… .  It keeps the path *after* /proxyAgent,
 | 
			
		||||
// // removes the two query parameters, disables HTTP/2 (mandatory for WS),
 | 
			
		||||
// // and streams with no buffering.
 | 
			
		||||
// func (a *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
//         ip   := r.URL.Query().Get("ip")
 | 
			
		||||
//         port := r.URL.Query().Get("port")
 | 
			
		||||
//         if ip == "" || port == "" {
 | 
			
		||||
//                 http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
 | 
			
		||||
//                 return
 | 
			
		||||
//         }
 | 
			
		||||
 | 
			
		||||
                        // Trim the first "/proxyAgent" prefix
 | 
			
		||||
                        if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
 | 
			
		||||
                                req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
 | 
			
		||||
                                if req.URL.Path == "" {
 | 
			
		||||
                                        req.URL.Path = "/"
 | 
			
		||||
                                }
 | 
			
		||||
                        }
 | 
			
		||||
//         // We leave the scheme "http" even for WebSockets – the Upgrade
 | 
			
		||||
//         // header does the rest.  (Only cosmetic to change it to "ws”.)
 | 
			
		||||
//         target, err := url.Parse("http://" + ip + ":" + port)
 | 
			
		||||
//         if err != nil {
 | 
			
		||||
//                 http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
 | 
			
		||||
//                 return
 | 
			
		||||
//         }
 | 
			
		||||
 | 
			
		||||
                        // Scrub ip/port from downstream query
 | 
			
		||||
                        q := req.URL.Query()
 | 
			
		||||
                        q.Del("ip")
 | 
			
		||||
                        q.Del("port")
 | 
			
		||||
                        req.URL.RawQuery = q.Encode()
 | 
			
		||||
//         proxy := &httputil.ReverseProxy{
 | 
			
		||||
//                 Director: func(req *http.Request) {
 | 
			
		||||
//                         // Point to the agent
 | 
			
		||||
//                         req.URL.Scheme = target.Scheme
 | 
			
		||||
//                         req.URL.Host   = target.Host
 | 
			
		||||
 | 
			
		||||
                        // Preserve Host (many CLI-style servers care)
 | 
			
		||||
                        req.Host = target.Host
 | 
			
		||||
                },
 | 
			
		||||
//                         // Trim the first "/proxyAgent" prefix
 | 
			
		||||
//                         if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
 | 
			
		||||
//                                 req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
 | 
			
		||||
//                                 if req.URL.Path == "" {
 | 
			
		||||
//                                         req.URL.Path = "/"
 | 
			
		||||
//                                 }
 | 
			
		||||
//                         }
 | 
			
		||||
 | 
			
		||||
                // Critical tweaks for WebSockets
 | 
			
		||||
                Transport: &http.Transport{
 | 
			
		||||
                        Proxy:               http.ProxyFromEnvironment,
 | 
			
		||||
                        DialContext:         (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
 | 
			
		||||
                        ForceAttemptHTTP2:   false,   // MUST be HTTP/1.1 for WS
 | 
			
		||||
                        ResponseHeaderTimeout: 0,
 | 
			
		||||
                },
 | 
			
		||||
//                         // Scrub ip/port from downstream query
 | 
			
		||||
//                         q := req.URL.Query()
 | 
			
		||||
//                         q.Del("ip")
 | 
			
		||||
//                         q.Del("port")
 | 
			
		||||
//                         req.URL.RawQuery = q.Encode()
 | 
			
		||||
 | 
			
		||||
                FlushInterval: -1,      // stream bytes immediately
 | 
			
		||||
        }
 | 
			
		||||
//                         // Preserve Host (many CLI-style servers care)
 | 
			
		||||
//                         req.Host = target.Host
 | 
			
		||||
//                 },
 | 
			
		||||
 | 
			
		||||
        proxy.ServeHTTP(w, r)
 | 
			
		||||
//                 // Critical tweaks for WebSockets
 | 
			
		||||
//                 Transport: &http.Transport{
 | 
			
		||||
//                         Proxy:               http.ProxyFromEnvironment,
 | 
			
		||||
//                         DialContext:         (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
 | 
			
		||||
//                         ForceAttemptHTTP2:   false,   // MUST be HTTP/1.1 for WS
 | 
			
		||||
//                         ResponseHeaderTimeout: 0,
 | 
			
		||||
//                 },
 | 
			
		||||
 | 
			
		||||
//                 FlushInterval: -1,      // stream bytes immediately
 | 
			
		||||
//         }
 | 
			
		||||
 | 
			
		||||
//         proxy.ServeHTTP(w, r)
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// Registration (keep the slash so sub‑paths are included):
 | 
			
		||||
//    webMux.HandleFunc("/proxyAgent/", app.proxyAgentHandler)
 | 
			
		||||
 | 
			
		||||
//func (app *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    // 0. Helpers for cookie handling
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    readCookie := func(name string) (string, bool) {
 | 
			
		||||
//        c, err := r.Cookie(name)
 | 
			
		||||
//        if err != nil { return "", false }
 | 
			
		||||
//        v, _ := url.QueryUnescape(c.Value)
 | 
			
		||||
//        return v, true
 | 
			
		||||
//    }
 | 
			
		||||
//    writeCookie := func(h http.Header, name, val string) {
 | 
			
		||||
//        h.Add("Set-Cookie", (&http.Cookie{
 | 
			
		||||
//            Name:     name,
 | 
			
		||||
//            Value:    url.QueryEscape(val),
 | 
			
		||||
//            Path:     "/proxyAgent",            // cookie visible only here
 | 
			
		||||
//            SameSite: http.SameSiteLaxMode,
 | 
			
		||||
//        }).String())
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    // 1. Decide which agent to talk to
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    ip, port := r.URL.Query().Get("ip"), r.URL.Query().Get("port")
 | 
			
		||||
//    // fallback to cookies if query params are absent
 | 
			
		||||
//    if ip == "" || port == "" {
 | 
			
		||||
//        if v, ok := readCookie("proxyagent_ip");   ok { ip = v }
 | 
			
		||||
//        if v, ok := readCookie("proxyagent_port"); ok { port = v }
 | 
			
		||||
//    }
 | 
			
		||||
//    if ip == "" || port == "" {
 | 
			
		||||
//        http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
 | 
			
		||||
//        return
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    target, err := url.Parse("http://" + ip + ":" + port)
 | 
			
		||||
//    if err != nil {
 | 
			
		||||
//        http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
 | 
			
		||||
//        return
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    // keep copies for ModifyResponse (Director will delete them)
 | 
			
		||||
//    ipForCookie, portForCookie := ip, port
 | 
			
		||||
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    // 2. Reverse proxy
 | 
			
		||||
//    //------------------------------------------------------------------
 | 
			
		||||
//    proxy := &httputil.ReverseProxy{
 | 
			
		||||
 | 
			
		||||
//        Director: func(req *http.Request) {
 | 
			
		||||
//            // 2.1 upstream address
 | 
			
		||||
//            req.URL.Scheme = target.Scheme
 | 
			
		||||
//            req.URL.Host   = target.Host
 | 
			
		||||
 | 
			
		||||
//            // 2.2 strip ONE leading /proxyAgent
 | 
			
		||||
//            if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
 | 
			
		||||
//                req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
 | 
			
		||||
//                if req.URL.Path == "" { req.URL.Path = "/" }
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            // 2.3 remove ip/port from outgoing query
 | 
			
		||||
//            q := req.URL.Query()
 | 
			
		||||
//            q.Del("ip"); q.Del("port")
 | 
			
		||||
//            req.URL.RawQuery = q.Encode()
 | 
			
		||||
 | 
			
		||||
//            // 2.4 keep original Host header
 | 
			
		||||
//            req.Host = target.Host
 | 
			
		||||
 | 
			
		||||
//            // 2.5 force uncompressed HTML so we can patch it
 | 
			
		||||
//            req.Header.Del("Accept-Encoding")
 | 
			
		||||
//        },
 | 
			
		||||
 | 
			
		||||
//        Transport: &http.Transport{
 | 
			
		||||
//            Proxy:               http.ProxyFromEnvironment,
 | 
			
		||||
//            DialContext:         (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
 | 
			
		||||
//            ForceAttemptHTTP2:   false,   // must stay on h1 for WebSocket upgrade
 | 
			
		||||
//            ResponseHeaderTimeout: 0,
 | 
			
		||||
//        },
 | 
			
		||||
 | 
			
		||||
//        FlushInterval: -1,                // instant streaming for xterm.js
 | 
			
		||||
 | 
			
		||||
//        //------------------------------------------------------------------
 | 
			
		||||
//        // 3.  Response rewrite & cookie injection
 | 
			
		||||
//        //------------------------------------------------------------------
 | 
			
		||||
//        ModifyResponse: func(resp *http.Response) error {
 | 
			
		||||
 | 
			
		||||
//            // 3.1 always (once per agent) set the cookies
 | 
			
		||||
//            writeCookie(resp.Header, "proxyagent_ip",   ipForCookie)
 | 
			
		||||
//            writeCookie(resp.Header, "proxyagent_port", portForCookie)
 | 
			
		||||
 | 
			
		||||
//            // 3.2 if the payload is HTML, inject <base href="/proxyAgent/">
 | 
			
		||||
//            if ct := resp.Header.Get("Content-Type"); strings.HasPrefix(ct, "text/html") {
 | 
			
		||||
//                body, _ := io.ReadAll(resp.Body)
 | 
			
		||||
//                patched := bytes.Replace(
 | 
			
		||||
//                    body,
 | 
			
		||||
//                    []byte("<head>"),
 | 
			
		||||
//                    []byte(`<head><base href="/proxyAgent/">`),
 | 
			
		||||
//                    1)
 | 
			
		||||
 | 
			
		||||
//                resp.Body          = io.NopCloser(bytes.NewReader(patched))
 | 
			
		||||
//                resp.ContentLength = int64(len(patched))
 | 
			
		||||
//                resp.Header.Set("Content-Length", strconv.Itoa(len(patched)))
 | 
			
		||||
//            }
 | 
			
		||||
 | 
			
		||||
//            return nil
 | 
			
		||||
//        },
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    proxy.ServeHTTP(w, r)
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
func (app *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
    // path is /proxyAgent/<agentKey>/whatever…
 | 
			
		||||
    tail := strings.TrimPrefix(r.URL.Path, "/proxyAgent/")
 | 
			
		||||
    parts := strings.SplitN(tail, "/", 2)
 | 
			
		||||
    if len(parts) < 1 || parts[0] == "" {
 | 
			
		||||
        http.Error(w, "missing agent key in path", http.StatusBadRequest)
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
    agentKey := parts[0]                   // "192.168.0.12:46059"
 | 
			
		||||
    upstreamPath := "/"
 | 
			
		||||
    if len(parts) == 2 { upstreamPath += parts[1] } // "whatever…"
 | 
			
		||||
 | 
			
		||||
    target, err := url.Parse("http://" + agentKey)
 | 
			
		||||
    if err != nil {
 | 
			
		||||
        http.Error(w, "invalid agent key", http.StatusBadRequest)
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    proxy := &httputil.ReverseProxy{
 | 
			
		||||
        Director: func(req *http.Request) {
 | 
			
		||||
            req.URL.Scheme = target.Scheme
 | 
			
		||||
            req.URL.Host   = target.Host
 | 
			
		||||
            req.URL.Path   = upstreamPath       // strip key
 | 
			
		||||
            req.Host       = target.Host
 | 
			
		||||
            req.Header.Del("Accept-Encoding")   // for HTML rewrite
 | 
			
		||||
        },
 | 
			
		||||
        Transport: &http.Transport{
 | 
			
		||||
            Proxy:               http.ProxyFromEnvironment,
 | 
			
		||||
            DialContext:         (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
 | 
			
		||||
            ForceAttemptHTTP2:   false,
 | 
			
		||||
        },
 | 
			
		||||
        FlushInterval: -1,
 | 
			
		||||
        ModifyResponse: func(resp *http.Response) error {
 | 
			
		||||
            if strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
 | 
			
		||||
                body, _ := io.ReadAll(resp.Body)
 | 
			
		||||
                patched := bytes.Replace(
 | 
			
		||||
                    body,
 | 
			
		||||
                    []byte("<head>"),
 | 
			
		||||
                    []byte(fmt.Sprintf(`<head><base href="/proxyAgent/%s/">`, agentKey)),
 | 
			
		||||
                    1)
 | 
			
		||||
                resp.Body          = io.NopCloser(bytes.NewReader(patched))
 | 
			
		||||
                resp.ContentLength = int64(len(patched))
 | 
			
		||||
                resp.Header.Set("Content-Length", strconv.Itoa(len(patched)))
 | 
			
		||||
            }
 | 
			
		||||
            return nil
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    proxy.ServeHTTP(w, r)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,52 +0,0 @@
 | 
			
		|||
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));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
const helpMsg = `
 | 
			
		||||
This is a shell in the browser, a web shell.
 | 
			
		||||
 | 
			
		||||
It includes a non-interactive shell with an optional interactive mode.
 | 
			
		||||
Furthermore, additional commands 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.
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,132 +0,0 @@
 | 
			
		|||
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);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,158 +0,0 @@
 | 
			
		|||
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…
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Helper: get ?ip=…&port=… from the current location
 | 
			
		||||
function getQueryParam(name) {
 | 
			
		||||
  return new URLSearchParams(window.location.search).get(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makeWsUrl() {
 | 
			
		||||
  const proxyIp   = getQueryParam("ip");     // "10.0.0.42" if you came via /proxyAgent
 | 
			
		||||
  const proxyPort = getQueryParam("port");
 | 
			
		||||
  const usingProxy = proxyIp && proxyPort;   // true only in that case
 | 
			
		||||
 | 
			
		||||
  if (usingProxy) {
 | 
			
		||||
    // Build ws(s)://<main-server>/proxyAgent/terminal?ip=…&port=…
 | 
			
		||||
    const u = new URL("/proxyAgent/terminal", window.location);
 | 
			
		||||
    u.searchParams.set("ip",   proxyIp);
 | 
			
		||||
    u.searchParams.set("port", proxyPort);
 | 
			
		||||
    u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
 | 
			
		||||
    return u.toString();
 | 
			
		||||
  }
 | 
			
		||||
  // Fallback: open directly on the agent we’re already on
 | 
			
		||||
  const u = new URL("/terminal", window.location);
 | 
			
		||||
  u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
 | 
			
		||||
  return u.toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  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(makeWsUrl());
 | 
			
		||||
  // 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,155 +0,0 @@
 | 
			
		|||
:root {
 | 
			
		||||
	--bg-color: #222;
 | 
			
		||||
	--text-color: #eee;
 | 
			
		||||
	--command-color: #75df0b;
 | 
			
		||||
	--error-color: #ff5555;
 | 
			
		||||
	--directory-color: #1bc9e7;
 | 
			
		||||
	--ps1-color: #75df0b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
	/* text-align: justify; */
 | 
			
		||||
	/* color: var(--text-color); */
 | 
			
		||||
	/* animation: 0.75s 2 changeColor; */
 | 
			
		||||
	animation: changeColor 1.75s forwards;
 | 
			
		||||
	/* max-width: 80ch; */
 | 
			
		||||
}
 | 
			
		||||
@keyframes changeColor {
 | 
			
		||||
	from { color: var(--command-color) };
 | 
			
		||||
	to { color: var(--text-color) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.error {
 | 
			
		||||
	/* text-align: justify; */
 | 
			
		||||
	color: var(--error-color);
 | 
			
		||||
	/* max-width: 80ch; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,46 +0,0 @@
 | 
			
		|||
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;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
 | 
			
		||||
//# sourceMappingURL=xterm-addon-fit.js.map
 | 
			
		||||
| 
						 | 
				
			
			@ -1,209 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Copyright (c) 2014 The xterm.js authors. All rights reserved.
 | 
			
		||||
 * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
 | 
			
		||||
 * https://github.com/chjj/term.js
 | 
			
		||||
 * @license MIT
 | 
			
		||||
 *
 | 
			
		||||
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
 * of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 * in the Software without restriction, including without limitation the rights
 | 
			
		||||
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
 * copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
 * furnished to do so, subject to the following conditions:
 | 
			
		||||
 *
 | 
			
		||||
 * The above copyright notice and this permission notice shall be included in
 | 
			
		||||
 * all copies or substantial portions of the Software.
 | 
			
		||||
 *
 | 
			
		||||
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
 * THE SOFTWARE.
 | 
			
		||||
 *
 | 
			
		||||
 * Originally forked from (with the author's permission):
 | 
			
		||||
 *   Fabrice Bellard's javascript vt100 for jslinux:
 | 
			
		||||
 *   http://bellard.org/jslinux/
 | 
			
		||||
 *   Copyright (c) 2011 Fabrice Bellard
 | 
			
		||||
 *   The original design remains. The terminal itself
 | 
			
		||||
 *   has been extended to include xterm CSI codes, among
 | 
			
		||||
 *   other features.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *  Default styles for xterm.js
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.xterm {
 | 
			
		||||
    cursor: text;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
    -ms-user-select: none;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm.focus,
 | 
			
		||||
.xterm:focus {
 | 
			
		||||
    outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-helpers {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    /**
 | 
			
		||||
     * The z-index of the helpers must be higher than the canvases in order for
 | 
			
		||||
     * IMEs to appear on top.
 | 
			
		||||
     */
 | 
			
		||||
    z-index: 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-helper-textarea {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    /* Move textarea out of the screen to the far left, so that the cursor is not visible */
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    left: -9999em;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    width: 0;
 | 
			
		||||
    height: 0;
 | 
			
		||||
    z-index: -5;
 | 
			
		||||
    /** Prevent wrapping so the IME appears against the textarea at the correct position */
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    resize: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .composition-view {
 | 
			
		||||
    /* TODO: Composition position got messed up somewhere */
 | 
			
		||||
    background: #000;
 | 
			
		||||
    color: #FFF;
 | 
			
		||||
    display: none;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .composition-view.active {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-viewport {
 | 
			
		||||
    /* On OS X this is required in order for the scroll bar to appear fully opaque */
 | 
			
		||||
    background-color: #000;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-screen {
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-screen canvas {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-scroll-area {
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-char-measure-element {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: -9999em;
 | 
			
		||||
    line-height: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm.enable-mouse-events {
 | 
			
		||||
    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
 | 
			
		||||
    cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm.xterm-cursor-pointer,
 | 
			
		||||
.xterm .xterm-cursor-pointer {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm.column-select.focus {
 | 
			
		||||
    /* Column selection mode */
 | 
			
		||||
    cursor: crosshair;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .xterm-accessibility,
 | 
			
		||||
.xterm .xterm-message {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
    color: transparent;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm .live-region {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: -9999px;
 | 
			
		||||
    width: 1px;
 | 
			
		||||
    height: 1px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-dim {
 | 
			
		||||
    /* Dim should not apply to background, so the opacity of the foreground color is applied
 | 
			
		||||
     * explicitly in the generated class and reset to 1 here */
 | 
			
		||||
    opacity: 1 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-underline-1 { text-decoration: underline; }
 | 
			
		||||
.xterm-underline-2 { text-decoration: double underline; }
 | 
			
		||||
.xterm-underline-3 { text-decoration: wavy underline; }
 | 
			
		||||
.xterm-underline-4 { text-decoration: dotted underline; }
 | 
			
		||||
.xterm-underline-5 { text-decoration: dashed underline; }
 | 
			
		||||
 | 
			
		||||
.xterm-overline {
 | 
			
		||||
    text-decoration: overline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
 | 
			
		||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
 | 
			
		||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
 | 
			
		||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
 | 
			
		||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
 | 
			
		||||
 | 
			
		||||
.xterm-strikethrough {
 | 
			
		||||
    text-decoration: line-through;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
 | 
			
		||||
	z-index: 6;
 | 
			
		||||
	position: absolute;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
 | 
			
		||||
	z-index: 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-decoration-overview-ruler {
 | 
			
		||||
    z-index: 8;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xterm-decoration-top {
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
				
			
			@ -12,9 +12,9 @@
 | 
			
		|||
    <script type="text/javascript" src="static/agents-graph.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="static/gontrol-helper.js"></script>
 | 
			
		||||
 | 
			
		||||
    <link rel="stylesheet" href="static/xterm/xterm.css" />
 | 
			
		||||
    <script rel="text/javascript" src="static/xterm/xterm.js"></script>
 | 
			
		||||
    <script rel="text/javascript" src="static/xterm/xterm-addon-fit.js"></script>
 | 
			
		||||
    <!-- <link rel="stylesheet" href="static/xterm/xterm.css" /> -->
 | 
			
		||||
    <!-- <script rel="text/javascript" src="static/xterm/xterm.js"></script> -->
 | 
			
		||||
    <!-- <script rel="text/javascript" src="static/xterm/xterm-addon-fit.js"></script> -->
 | 
			
		||||
 | 
			
		||||
    <title>g2: gommand & gontrol</title>
 | 
			
		||||
</head>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,13 @@
 | 
			
		|||
    <p>Hostname: {{.HostName}}</p>
 | 
			
		||||
    <!-- <button class="btn btn-warning" hx-get="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="#agentConnect" hx-swap="innerHTML">Proxy</button> -->
 | 
			
		||||
    <a class="btn btn-info"
 | 
			
		||||
        href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}"
 | 
			
		||||
        href="/proxyAgent/{{.IPv4Address}}:{{.AddPort}}/"
 | 
			
		||||
        hx-target="_blank"
 | 
			
		||||
        target="_blank">Connect via Proxy</a>
 | 
			
		||||
    <!-- <a class="btn btn-info" -->
 | 
			
		||||
    <!--     href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" -->
 | 
			
		||||
    <!--     hx-target="_blank" -->
 | 
			
		||||
    <!--     target="_blank">Connect via Proxy</a> -->
 | 
			
		||||
    <a href="http://{{.IPv4Address}}:{{.AddPort}}" class="btn btn-outline-info" target="_blank">Connect without Proxy</a>
 | 
			
		||||
    <!-- <a href="http://{{.IPv4Address}}:{{.AddPort}}" target="_blank">Connect without Proxy</a> -->
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue