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;
function initializeCytoscape() {
if (isCyInitialized) return;
if (isCyInitialized) return;
cy = cytoscape({
container: document.getElementById('cyto-graph'),
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
/* --- single, central stylesheet -------------------------------- */
style: [
/* base node look (shared) */
/* background-opacity: 0 will remove the node color and background of the svg */
{
selector: 'node',
style: {
'width' : 50,
'height' : 50,
'label' : 'data(name)',
'text-outline-width' : 2,
'text-outline-color' : '#333',
'color' : '#fff',
'background-opacity' : 1,
'background-color': '#f8f9fa'
}
});
},
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
@ -57,108 +89,58 @@ document.addEventListener('DOMContentLoaded', function () {
// Load the graph after HTMX swap
async function updateGraph(agentData) {
if (!cy) {
console.error('Cytoscape is not initialized yet.');
return;
}
if (!cy) {
console.error('Cytoscape not initialised');
return;
}
// console.log('Updating graph with agent data:', agentData);
cy.elements().remove(); // clear existing
// Clear existing nodes and edges
cy.elements().remove();
/* --- add agent nodes ------------------------------------------- */
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
agentData.forEach(agent => {
const id = agent.agentId;
const name = agent.agentName;
const status = agent.status;
if (!id || !name) return;
if (id && name) {
// let nodeColor = (status === 'Connected') ? '#28a745' : '#dc3545'; // Green for connected, Red for disconnected
let nodeBg = (status === 'Connected') ? 'url(static/computer-online.svg)' : 'url(static/computer-offline.svg)'; // 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': '#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);
}
cy.add({
group : 'nodes',
data : {
id : id,
name : name,
type : 'Agent',
ip : agent.IPv4Address,
status: agent.status
},
classes: online ? 'online' : 'offline' // ← only classes
});
});
// 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
'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);
}
/* --- ensure target/server node exists --------------------------- */
const targetId = 'g2';
if (cy.getElementById(targetId).length === 0) {
cy.add({
group : 'nodes',
data : { id: targetId, name: 'g2' },
classes: 'server'
});
}
// Force a layout update
cy.layout({
name: 'grid',
rows: 2
}).run();
/* --- connect every agent to the target ------------------------- */
agentData.forEach(agent => {
if (agent.agentId) {
cy.add({
group: 'edges',
data : { source: agent.agentId, target: targetId }
});
}
});
/* --- relayout --------------------------------------------------- */
cy.layout({ name: 'grid', rows: 2 }).run();
}
async function fetchData() {
const url = "http://localhost:3333/agents";
try {
@ -176,7 +158,7 @@ async function fetchData() {
// Function to get agent data and update the graph
async function loadGraphData() {
console.log("Function loadGraphData()");
// console.log("Function loadGraphData()");
// Fetch agent data asynchronously
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) {
if (event.target.id === "agentList") {
restoreCheckboxState();
updateAgentDropdown();
updateAgentStatus();
bindRowClicks();
}
@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
restoreCheckboxState();
themeToggle();
focusCommandInput();
});
let cachedAgentNames = '';
@ -83,6 +84,7 @@ function restoreCheckboxState() {
});
}
// Because of this function, you can click anywhere on the row to select it
function bindRowClicks() {
const rows = document.querySelectorAll('#agentList tbody tr');
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 optionValues = Array.from(select.options).map(opt => opt.value);
const rows = document.querySelectorAll("#agentList tbody tr");
@ -130,6 +134,12 @@ function updateAgentDropdown() {
});
}
////////////////////////////////////////////////////////////////////////////////
//
// Toggle them icon from sun/moon on button click
//
////////////////////////////////////////////////////////////////////////////////
function themeToggle() {
const body = document.body;
const toggleBtn = document.getElementById('themeToggleButton');
@ -139,10 +149,10 @@ function themeToggle() {
body.classList.remove('bg-light', 'text-dark', 'bg-dark', 'text-light');
if (theme === 'dark') {
body.classList.add('bg-dark', 'text-light');
toggleBtn.innerHTML = '<i class="bi bi-moon"></i>';
toggleBtn.innerHTML = '☽'; // Could switch to the colored version 🌙
} else {
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
lines = lines.map(line => {
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)) {
return `<span style="color: blue;">${line}</span>`;
return `<span style="color: var(--bs-primary); font-size: 16px">${line}</span>`;
} 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
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 */
border: 1px solid #fff; /* optional: for visual clarity */
padding: 10px; /* optional: spacing inside the container */
line-height: 1em;
/* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
}
.log-info, .log-warning, .log-error, .log-fatal, .log-debug{ font-family: "Lucida Console", Monaco, monospace;
font-size: 12px;
font-size: 13px;
}
.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 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 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">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@1.9.12"></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/bootstrap/bootstrap.bundle.min.js"></script>
<script type="text/javascript" src="static/htmx/htmx.org@1.9.12.js"></script>
<script type="text/javascript" src="static/cytoscape.min.js"></script>
<script type="text/javascript" src="static/agents-graph.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">
<button id="themeToggleButton"
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">
<i class="bi bi-sun"></i>
</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">
<!-- Agent List -->
<div class="col-12 col-md-6">
@ -121,7 +119,7 @@
<select id="modalAgentName"
class="form-select d-none"
name="agentName"
hx-on="htmx:afterSwap:updateAgentDropdown">
hx-on="htmx:afterSwap:updateAgentStatus">
<option value="" disabled selected>Select an Agent</option>
</select>
</form>
@ -147,7 +145,7 @@
<select id="agentName"
class="form-select d-none"
name="agentName"
hx-on="htmx:afterSwap:updateAgentDropdown">
hx-on="htmx:afterSwap:updateAgentStatus">
<option value="" disabled selected>Select an Agent</option>
</select>
</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))
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))
if ch, ok := responseChannels.Load(agentName); ok {