fixed interactive terminal through proxyAgent
This commit is contained in:
		
							parent
							
								
									7472e2384e
								
							
						
					
					
						commit
						a03b419b43
					
				
							
								
								
									
										230
									
								
								main.go
								
								
								
								
							
							
						
						
									
										230
									
								
								main.go
								
								
								
								
							| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
| 
						 | 
				
			
			@ -40,8 +41,8 @@ var (
 | 
			
		|||
//go:embed static/*
 | 
			
		||||
var staticFiles embed.FS
 | 
			
		||||
 | 
			
		||||
//go:embed templates
 | 
			
		||||
var templateFiles embed.FS
 | 
			
		||||
// //go:embed templates
 | 
			
		||||
// var templateFiles embed.FS
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Database struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,8 +69,20 @@ func processError(err error) {
 | 
			
		|||
func init() {
 | 
			
		||||
	tmpl, _ = template.ParseGlob("templates/*.html")
 | 
			
		||||
 | 
			
		||||
	// var err error
 | 
			
		||||
	// tmpl, err = template.ParseFS(
 | 
			
		||||
	// 	templateFiles,
 | 
			
		||||
	// 	"templates/*.html",
 | 
			
		||||
	// 	"templates/partials/*.html",
 | 
			
		||||
	// )
 | 
			
		||||
 | 
			
		||||
	// if err != nil {
 | 
			
		||||
	// 	log.Fatalf("Failed to parse embedded templates: %v", err)
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// Sqlite3
 | 
			
		||||
	err := logger.InitDB("/tmp/gontrol_logs.db")
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +91,19 @@ func init() {
 | 
			
		|||
	db = database.InitSQLiteDB("/tmp/gontrol_agents.db")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// func renderTemplate(w http.ResponseWriter, tmplPath string, data interface{}) {
 | 
			
		||||
// 	t := tmpl.Lookup(strings.TrimPrefix(tmplPath, "templates/"))
 | 
			
		||||
// 	if t == nil {
 | 
			
		||||
// 		log.Printf("Template %s not found", tmplPath)
 | 
			
		||||
// 		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
// 		return
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
// 	if err := t.Execute(w, data); err != nil {
 | 
			
		||||
// 		log.Printf("Failed to render template %s: %v", tmplPath, err)
 | 
			
		||||
// 		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
 | 
			
		||||
	t, err := template.ParseFiles(tmpl)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +135,7 @@ func agentsHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
		} else {
 | 
			
		||||
			agent, _ := api.GetAgent(db, w, r, agentId)
 | 
			
		||||
			renderTemplate(w, "templates/partials/agent_detail.html", agent)
 | 
			
		||||
			// renderTemplate(w, "agent_detail.html", agent)
 | 
			
		||||
		}
 | 
			
		||||
	case http.MethodPost:
 | 
			
		||||
		api.CreateAgent(db, w, r)
 | 
			
		||||
| 
						 | 
				
			
			@ -155,6 +182,7 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	renderTemplate(w, "templates/partials/agent_list.html", agents)
 | 
			
		||||
	// renderTemplate(w, "agent_list.html", agents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getAgentsStatus() []string {
 | 
			
		||||
| 
						 | 
				
			
			@ -218,167 +246,67 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
	renderTemplate(w, "templates/partials/logs_partial.html", logs)
 | 
			
		||||
	// renderTemplate(w, "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.
 | 
			
		||||
// 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 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
 | 
			
		||||
	}
 | 
			
		||||
        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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
	agentBaseURL := "http://" + agentIP + ":" + agentPort
 | 
			
		||||
        // 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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
	targetURL, err := url.Parse(agentBaseURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, "Invalid agent URL: "+err.Error(), http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
        proxy := &httputil.ReverseProxy{
 | 
			
		||||
                Director: func(req *http.Request) {
 | 
			
		||||
                        // Point to the agent
 | 
			
		||||
                        req.URL.Scheme = target.Scheme
 | 
			
		||||
                        req.URL.Host   = target.Host
 | 
			
		||||
 | 
			
		||||
	proxy := httputil.NewSingleHostReverseProxy(targetURL)
 | 
			
		||||
                        // 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 = "/"
 | 
			
		||||
                                }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
	// Modify the Director function to rewrite the request URL before sending to agent
 | 
			
		||||
	originalDirector := proxy.Director
 | 
			
		||||
	proxy.Director = func(req *http.Request) {
 | 
			
		||||
		originalDirector(req)
 | 
			
		||||
                        // Scrub ip/port from downstream query
 | 
			
		||||
                        q := req.URL.Query()
 | 
			
		||||
                        q.Del("ip")
 | 
			
		||||
                        q.Del("port")
 | 
			
		||||
                        req.URL.RawQuery = q.Encode()
 | 
			
		||||
 | 
			
		||||
		// 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 = "/"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
                        // Preserve Host (many CLI-style servers care)
 | 
			
		||||
                        req.Host = target.Host
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
		// 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()
 | 
			
		||||
                // 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,
 | 
			
		||||
                },
 | 
			
		||||
 | 
			
		||||
		// Optional: set the Host header to the target host
 | 
			
		||||
		req.Host = targetURL.Host
 | 
			
		||||
	}
 | 
			
		||||
                FlushInterval: -1,      // stream bytes immediately
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
	proxy.ServeHTTP(w, r)
 | 
			
		||||
        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() {
 | 
			
		||||
 | 
			
		||||
	// sqlite3 has been initialized in init()
 | 
			
		||||
| 
						 | 
				
			
			@ -405,8 +333,8 @@ func main() {
 | 
			
		|||
	webMux.HandleFunc("/logs", logsHandler)
 | 
			
		||||
	webMux.HandleFunc("/logs/{level}", logsHandler)
 | 
			
		||||
    webMux.Handle("/static/", http.FileServer(http.FS(staticFiles)))
 | 
			
		||||
    webMux.Handle("/templates/", http.FileServer(http.FS(templateFiles)))
 | 
			
		||||
	webMux.HandleFunc("/proxyAgent", proxyAgentHandler)
 | 
			
		||||
	webMux.HandleFunc("/proxyAgent/", proxyAgentHandler)
 | 
			
		||||
 | 
			
		||||
	// db := database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
 | 
			
		||||
	// defer db.Close()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
			
		|||
    if (!interactiveMode && event.key === "Enter") {
 | 
			
		||||
      const command = input.value.trim();
 | 
			
		||||
      if (command === "start-interactive") {
 | 
			
		||||
 | 
			
		||||
        startInteractiveSession();
 | 
			
		||||
        input.value = "";
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +20,30 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
			
		|||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 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");   // "8080"
 | 
			
		||||
const usingProxy = proxyIp && proxyPort;   // truthy 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +80,8 @@ document.addEventListener("DOMContentLoaded", function () {
 | 
			
		|||
    console.log("Initial fit: container width =", xtermContainer.offsetWidth, "cols =", term.cols);
 | 
			
		||||
  }, 100);
 | 
			
		||||
 | 
			
		||||
  interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
 | 
			
		||||
  interactiveWS = new WebSocket(makeWsUrl());
 | 
			
		||||
  // interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
 | 
			
		||||
  interactiveWS.binaryType = "arraybuffer";
 | 
			
		||||
 | 
			
		||||
  interactiveWS.onopen = function () {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
!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
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,209 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 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
											
										
									
								
							| 
						 | 
				
			
			@ -13,6 +13,11 @@
 | 
			
		|||
    <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.23/dist/cytoscape.min.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="static/agents-graph.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="static/gontrol-helper.js"></script>
 | 
			
		||||
 | 
			
		||||
    <link rel="stylesheet" href="static/xterm.css" />
 | 
			
		||||
    <script rel="text/javascript" src="static/xterm.js"></script>
 | 
			
		||||
    <script rel="text/javascript" src="static/xterm-addon-fit.js"></script>
 | 
			
		||||
 | 
			
		||||
    <title>g2: gommand & gontrol</title>
 | 
			
		||||
</head>
 | 
			
		||||
<!-- <body class="bg-dark text-light" data-bs-theme="dark"> -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue