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

@ -8,37 +8,69 @@ function initializeCytoscape() {
cy = cytoscape({
container: document.getElementById('cyto-graph'),
/* --- single, central stylesheet -------------------------------- */
style: [
/* base node look (shared) */
/* background-opacity: 0 will remove the node color and background of the svg */
{
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'
'width' : 50,
'height' : 50,
'label' : 'data(name)',
'text-outline-width' : 2,
'text-outline-color' : '#333',
'color' : '#fff',
'background-opacity' : 1,
'background-color': '#f8f9fa'
}
},
/* 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',
'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
}
layout: { name: 'grid', rows: 2 }
});
isCyInitialized = true; // Mark Cytoscape as initialized
isCyInitialized = true;
}
// Load the graph after the page has fully loaded
@ -58,107 +90,57 @@ document.addEventListener('DOMContentLoaded', function () {
async function updateGraph(agentData) {
if (!cy) {
console.error('Cytoscape is not initialized yet.');
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 nodes for each agent with the AgentName as the label
/* --- add agent nodes ------------------------------------------- */
agentData.forEach(agent => {
const id = agent.agentId;
const name = agent.agentName;
const status = agent.status;
const online = agent.status === 'Connected';
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
if (!id || !name) return;
cy.add({
group: 'nodes',
data: {
id: id,
name: name,
status: status,
type: agent.agentType,
ip: agent.IPv4Address
group : 'nodes',
data : {
id : id,
name : name,
type : 'Agent',
ip : agent.IPv4Address,
status: agent.status
},
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'
}
classes: online ? 'online' : 'offline' // ← only classes
});
} 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) {
/* --- ensure target/server node exists --------------------------- */
const targetId = 'g2';
if (cy.getElementById(targetId).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'
}
group : 'nodes',
data : { id: targetId, name: 'g2' },
classes: 'server'
});
}
// Connect each agent to the target node (`g2`)
/* --- connect every agent to the target ------------------------- */
agentData.forEach(agent => {
const id = agent.agentId;
if (id) {
if (agent.agentId) {
cy.add({
group: 'edges',
data: {
source: id,
target: targetNode
}
data : { source: agent.agentId, target: targetId }
});
} else {
console.warn('Skipping edge for agent with missing agentId:', agent);
}
});
// Force a layout update
cy.layout({
name: 'grid',
rows: 2
}).run();
/* --- 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 {