cleanup and styling
|
@ -8,19 +8,50 @@ function initializeCytoscape() {
|
||||||
cy = cytoscape({
|
cy = cytoscape({
|
||||||
container: document.getElementById('cyto-graph'),
|
container: document.getElementById('cyto-graph'),
|
||||||
|
|
||||||
|
/* --- single, central stylesheet -------------------------------- */
|
||||||
style: [
|
style: [
|
||||||
|
/* base node look (shared) */
|
||||||
|
/* background-opacity: 0 will remove the node color and background of the svg */
|
||||||
{
|
{
|
||||||
selector: 'node',
|
selector: 'node',
|
||||||
style: {
|
style: {
|
||||||
'background-color': '#007bff',
|
'width' : 50,
|
||||||
'label': 'data(name)', // Ensure the label uses the AgentName
|
'height' : 50,
|
||||||
'color': 'white',
|
'label' : 'data(name)',
|
||||||
'text-outline-width' : 2,
|
'text-outline-width' : 2,
|
||||||
'text-outline-color' : '#333',
|
'text-outline-color' : '#333',
|
||||||
'width': '50px',
|
'color' : '#fff',
|
||||||
'height': '50px'
|
'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',
|
selector: 'edge',
|
||||||
style: {
|
style: {
|
||||||
|
@ -29,16 +60,17 @@ function initializeCytoscape() {
|
||||||
'target-arrow-color': '#ccc',
|
'target-arrow-color': '#ccc',
|
||||||
'target-arrow-shape': 'triangle'
|
'target-arrow-shape': 'triangle'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node.darkTheme',
|
||||||
|
style : { 'background-color': '#212529' }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
layout: {
|
layout: { name: 'grid', rows: 2 }
|
||||||
name: 'grid',
|
|
||||||
rows: 2
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
isCyInitialized = true; // Mark Cytoscape as initialized
|
isCyInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the graph after the page has fully loaded
|
// Load the graph after the page has fully loaded
|
||||||
|
@ -58,107 +90,57 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Add nodes for each agent with the AgentName as the label
|
|
||||||
agentData.forEach(agent => {
|
agentData.forEach(agent => {
|
||||||
const id = agent.agentId;
|
const id = agent.agentId;
|
||||||
const name = agent.agentName;
|
const name = agent.agentName;
|
||||||
const status = agent.status;
|
const online = agent.status === 'Connected';
|
||||||
|
|
||||||
if (id && name) {
|
if (!id || !name) return;
|
||||||
// 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({
|
cy.add({
|
||||||
group : 'nodes',
|
group : 'nodes',
|
||||||
data : {
|
data : {
|
||||||
id : id,
|
id : id,
|
||||||
name : name,
|
name : name,
|
||||||
status: status,
|
type : 'Agent',
|
||||||
type: agent.agentType,
|
ip : agent.IPv4Address,
|
||||||
ip: agent.IPv4Address
|
status: agent.status
|
||||||
},
|
},
|
||||||
style: {
|
classes: online ? 'online' : 'offline' // ← only classes
|
||||||
// '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
|
|
||||||
if (cy.getElementById(targetNode).length === 0) {
|
|
||||||
cy.add({
|
cy.add({
|
||||||
group : 'nodes',
|
group : 'nodes',
|
||||||
data: {
|
data : { id: targetId, name: 'g2' },
|
||||||
id: targetNode,
|
classes: 'server'
|
||||||
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`)
|
/* --- connect every agent to the target ------------------------- */
|
||||||
agentData.forEach(agent => {
|
agentData.forEach(agent => {
|
||||||
const id = agent.agentId;
|
if (agent.agentId) {
|
||||||
if (id) {
|
|
||||||
cy.add({
|
cy.add({
|
||||||
group: 'edges',
|
group: 'edges',
|
||||||
data: {
|
data : { source: agent.agentId, target: targetId }
|
||||||
source: id,
|
|
||||||
target: targetNode
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.warn('Skipping edge for agent with missing agentId:', agent);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force a layout update
|
/* --- relayout --------------------------------------------------- */
|
||||||
cy.layout({
|
cy.layout({ name: 'grid', rows: 2 }).run();
|
||||||
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();
|
||||||
|
|
|
@ -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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 652 B |
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 674 B |
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 673 B |
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|