cleanup and styling

This commit is contained in:
Stefan Etringer 2025-07-08 12:12:39 +00:00
parent a4e889f232
commit 7a4135351f
13 changed files with 197 additions and 145 deletions

View File

@ -3,42 +3,74 @@ let isCyInitialized = false;
let cy; let cy;
function initializeCytoscape() { function initializeCytoscape() {
if (isCyInitialized) return; if (isCyInitialized) return;
cy = cytoscape({ cy = cytoscape({
container: document.getElementById('cyto-graph'), container: document.getElementById('cyto-graph'),
style: [ /* --- single, central stylesheet -------------------------------- */
{ style: [
selector: 'node', /* base node look (shared) */
style: { /* background-opacity: 0 will remove the node color and background of the svg */
'background-color': '#007bff', {
'label': 'data(name)', // Ensure the label uses the AgentName selector: 'node',
'color': 'white', style: {
'text-outline-width': 2, 'width' : 50,
'text-outline-color': '#333', 'height' : 50,
'width': '50px', 'label' : 'data(name)',
'height': '50px' 'text-outline-width' : 2,
} 'text-outline-color' : '#333',
}, 'color' : '#fff',
{ 'background-opacity' : 1,
selector: 'edge', 'background-color': '#f8f9fa'
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 /* agent nodes — online / offline variants */
{
selector: 'node.online[type = "Agent"]',
style: {
'background-image' : 'url(static/img/computer-online.svg)',
'background-fit' : 'contain'
}
},
{
selector: 'node.offline[type = "Agent"]',
style: {
'background-image' : 'url(static/img/computer-offline.svg)',
'background-fit' : 'contain'
}
},
/* target / server node */
{
selector: 'node.server',
style: {
'background-image' : 'url(static/img/server-online.svg)',
'background-fit' : 'contain'
}
},
/* edge style */
{
selector: 'edge',
style: {
'width' : 3,
'line-color' : '#ccc',
'target-arrow-color': '#ccc',
'target-arrow-shape': 'triangle'
}
},
{
selector: 'node.darkTheme',
style : { 'background-color': '#212529' }
}
],
layout: { name: 'grid', rows: 2 }
});
isCyInitialized = true;
} }
// Load the graph after the page has fully loaded // Load the graph after the page has fully loaded
@ -57,108 +89,58 @@ document.addEventListener('DOMContentLoaded', function () {
// Load the graph after HTMX swap // Load the graph after HTMX swap
async function updateGraph(agentData) { async function updateGraph(agentData) {
if (!cy) { if (!cy) {
console.error('Cytoscape is not initialized yet.'); console.error('Cytoscape not initialised');
return; return;
} }
// console.log('Updating graph with agent data:', agentData); cy.elements().remove(); // clear existing
// Clear existing nodes and edges /* --- add agent nodes ------------------------------------------- */
cy.elements().remove(); agentData.forEach(agent => {
const id = agent.agentId;
const name = agent.agentName;
const online = agent.status === 'Connected';
// Add nodes for each agent with the AgentName as the label if (!id || !name) return;
agentData.forEach(agent => {
const id = agent.agentId;
const name = agent.agentName;
const status = agent.status;
if (id && name) { cy.add({
// let nodeColor = (status === 'Connected') ? '#28a745' : '#dc3545'; // Green for connected, Red for disconnected group : 'nodes',
let nodeBg = (status === 'Connected') ? 'url(static/computer-online.svg)' : 'url(static/computer-offline.svg)'; // Green for connected, Red for disconnected data : {
id : id,
cy.add({ name : name,
group: 'nodes', type : 'Agent',
data: { ip : agent.IPv4Address,
id: id, status: agent.status
name: name, },
status: status, classes: online ? 'online' : 'offline' // ← only classes
type: agent.agentType,
ip: agent.IPv4Address
},
style: {
// 'background-color': '#f8f9fa',
'background-opacity': '0',
'background-image': nodeBg,
'background-fit': 'contain',
'background-clip': 'none',
'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) /* --- ensure target/server node exists --------------------------- */
const targetNode = 'g2'; const targetId = 'g2';
if (cy.getElementById(targetId).length === 0) {
// Ensure the target node (`g2`) exists, if not, create it cy.add({
if (cy.getElementById(targetNode).length === 0) { group : 'nodes',
cy.add({ data : { id: targetId, name: 'g2' },
group: 'nodes', classes: 'server'
data: {
id: targetNode,
name: 'g2',
status: 'Target',
type: 'Server',
ip: 'N/A'
},
style: {
// 'background-color': '#6c757d', // Gray for target node
'background-opacity': '0',
'background-image': 'url(static/server-online.svg)',
'background-fit': 'contain',
'background-clip': 'none',
'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 /* --- connect every agent to the target ------------------------- */
cy.layout({ agentData.forEach(agent => {
name: 'grid', if (agent.agentId) {
rows: 2 cy.add({
}).run(); group: 'edges',
data : { source: agent.agentId, target: targetId }
});
}
});
/* --- relayout --------------------------------------------------- */
cy.layout({ name: 'grid', rows: 2 }).run();
} }
async function fetchData() { async function fetchData() {
const url = "http://localhost:3333/agents"; const url = "http://localhost:3333/agents";
try { try {
@ -176,7 +158,7 @@ async function fetchData() {
// Function to get agent data and update the graph // Function to get agent data and update the graph
async function loadGraphData() { async function loadGraphData() {
console.log("Function loadGraphData()"); // console.log("Function loadGraphData()");
// Fetch agent data asynchronously // Fetch agent data asynchronously
agentData = await fetchData(); agentData = await fetchData();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('htmx:afterSwap', function(event) { document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.target.id === "agentList") { if (event.target.id === "agentList") {
restoreCheckboxState(); restoreCheckboxState();
updateAgentDropdown(); updateAgentStatus();
bindRowClicks(); bindRowClicks();
} }
@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
restoreCheckboxState(); restoreCheckboxState();
themeToggle(); themeToggle();
focusCommandInput();
}); });
let cachedAgentNames = ''; let cachedAgentNames = '';
@ -83,6 +84,7 @@ function restoreCheckboxState() {
}); });
} }
// Because of this function, you can click anywhere on the row to select it
function bindRowClicks() { function bindRowClicks() {
const rows = document.querySelectorAll('#agentList tbody tr'); const rows = document.querySelectorAll('#agentList tbody tr');
rows.forEach(row => { rows.forEach(row => {
@ -101,7 +103,9 @@ function bindRowClicks() {
}); });
} }
function updateAgentDropdown() {
// Colorize connection status inside agent list
function updateAgentStatus() {
const select = document.getElementById("agentName"); const select = document.getElementById("agentName");
const optionValues = Array.from(select.options).map(opt => opt.value); const optionValues = Array.from(select.options).map(opt => opt.value);
const rows = document.querySelectorAll("#agentList tbody tr"); const rows = document.querySelectorAll("#agentList tbody tr");
@ -130,6 +134,12 @@ function updateAgentDropdown() {
}); });
} }
////////////////////////////////////////////////////////////////////////////////
//
// Toggle them icon from sun/moon on button click
//
////////////////////////////////////////////////////////////////////////////////
function themeToggle() { function themeToggle() {
const body = document.body; const body = document.body;
const toggleBtn = document.getElementById('themeToggleButton'); const toggleBtn = document.getElementById('themeToggleButton');
@ -139,10 +149,10 @@ function themeToggle() {
body.classList.remove('bg-light', 'text-dark', 'bg-dark', 'text-light'); body.classList.remove('bg-light', 'text-dark', 'bg-dark', 'text-light');
if (theme === 'dark') { if (theme === 'dark') {
body.classList.add('bg-dark', 'text-light'); body.classList.add('bg-dark', 'text-light');
toggleBtn.innerHTML = '<i class="bi bi-moon"></i>'; toggleBtn.innerHTML = '☽'; // Could switch to the colored version 🌙
} else { } else {
body.classList.add('bg-light', 'text-dark'); body.classList.add('bg-light', 'text-dark');
toggleBtn.innerHTML = '<i class="bi bi-sun"></i>'; toggleBtn.innerHTML = '☼'; // Could switch to the colored version ☀
} }
} }
@ -171,14 +181,28 @@ function styleCommandOutput() {
// Map lines: if contains "Error", wrap in red span, else plain text // Map lines: if contains "Error", wrap in red span, else plain text
lines = lines.map(line => { lines = lines.map(line => {
if (line.includes("ERROR")) { if (line.includes("ERROR")) {
return `<span style="color: red;">${line}</span>`; return `<span style="color: var(--bs-danger);">${line}</span>`;
} else if (/^\[\S*\]$/.test(line)) { } else if (/^\[\S*\]$/.test(line)) {
return `<span style="color: blue;">${line}</span>`; return `<span style="color: var(--bs-primary); font-size: 16px">${line}</span>`;
} else { } else {
return `<span style="font-size: 16px">${line}</span>`; return `<span style="font-size: 16px; line-height: 1em;">${line}</span>`;
} }
}); });
// Replace innerHTML with the joined lines, joined by <br> for line breaks in HTML // Replace innerHTML with the joined lines, joined by <br> for line breaks in HTML
commandOutput.innerHTML = lines.join('<br>'); commandOutput.innerHTML = lines.join('<br>');
} }
////////////////////////////////////////////////////////////////////////////////
//
// Focus on modal command inpu
//
////////////////////////////////////////////////////////////////////////////////
function focusCommandInput() {
// const modalEl = document.getElementById('exampleModal');
var modalElement = document.getElementById('exampleModal')
modalElement.addEventListener('shown.bs.modal', event => {
document.getElementById('modalCommand').focus();
})
}

View File

@ -15,11 +15,12 @@
overflow-y: auto; /* enables vertical scroll when content overflows */ overflow-y: auto; /* enables vertical scroll when content overflows */
border: 1px solid #fff; /* optional: for visual clarity */ border: 1px solid #fff; /* optional: for visual clarity */
padding: 10px; /* optional: spacing inside the container */ padding: 10px; /* optional: spacing inside the container */
line-height: 1em;
/* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */ /* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
} }
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{ font-family: "Lucida Console", Monaco, monospace; .log-info, .log-warning, .log-error, .log-fatal, .log-debug{ font-family: "Lucida Console", Monaco, monospace;
font-size: 12px; font-size: 13px;
} }
.log-info { .log-info {

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 655 B

View File

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

View File

Before

Width:  |  Height:  |  Size: 674 B

After

Width:  |  Height:  |  Size: 674 B

View File

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 673 B

View File

@ -4,13 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <link href="static/bootstrap/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="static/gontrol-stylesheet.css"> <link rel="stylesheet" type="text/css" href="static/gontrol-stylesheet.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script type="text/javascript" src="static/bootstrap/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet"> <script type="text/javascript" src="static/htmx/htmx.org@1.9.12.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script type="text/javascript" src="static/cytoscape.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/agents-graph.js"></script>
<script type="text/javascript" src="static/gontrol-helper.js"></script> <script type="text/javascript" src="static/gontrol-helper.js"></script>
@ -24,12 +22,12 @@
<body class="bg-light text-dark" data-bs-theme="light"> <body class="bg-light text-dark" data-bs-theme="light">
<button id="themeToggleButton" <button id="themeToggleButton"
class="btn btn-outline-secondary position-fixed" class="btn btn-outline-secondary position-fixed"
style="top: 1rem; right: 1rem; z-index: 1;" style="top: 1rem; right: 1.5rem; z-index: 1; border-color: var(--bs-tertiary-color);"
aria-label="Toggle Theme"> aria-label="Toggle Theme">
<i class="bi bi-sun"></i>
</button> </button>
<div class="container-fluid px-4 py-3 pb-5"> <div class="container-fluid px-4 py-4 pb-5">
<div class="row g-2"> <div class="row g-2">
<!-- Agent List --> <!-- Agent List -->
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@ -121,7 +119,7 @@
<select id="modalAgentName" <select id="modalAgentName"
class="form-select d-none" class="form-select d-none"
name="agentName" name="agentName"
hx-on="htmx:afterSwap:updateAgentDropdown"> hx-on="htmx:afterSwap:updateAgentStatus">
<option value="" disabled selected>Select an Agent</option> <option value="" disabled selected>Select an Agent</option>
</select> </select>
</form> </form>
@ -147,7 +145,7 @@
<select id="agentName" <select id="agentName"
class="form-select d-none" class="form-select d-none"
name="agentName" name="agentName"
hx-on="htmx:afterSwap:updateAgentDropdown"> hx-on="htmx:afterSwap:updateAgentStatus">
<option value="" disabled selected>Select an Agent</option> <option value="" disabled selected>Select an Agent</option>
</select> </select>
</form> </form>

View File

@ -228,7 +228,8 @@ func (wsh webSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
logger.InsertLog(logger.Error, fmt.Sprintf("Error reading from agent %s: %v", agentName, err)) logger.InsertLog(logger.Error, fmt.Sprintf("Error reading from agent %s: %v", agentName, err))
break break
} }
log.Printf("Message from agent %s: %s", agentName, message) // log.Printf("Message from agent %s: %s", agentName, message)
log.Printf("Message from agent %s received", agentName)
logger.InsertLog(logger.Debug, fmt.Sprintf("Message from agent %s: %s", agentName, message)) logger.InsertLog(logger.Debug, fmt.Sprintf("Message from agent %s: %s", agentName, message))
if ch, ok := responseChannels.Load(agentName); ok { if ch, ok := responseChannels.Load(agentName); ok {