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