It works now
This commit is contained in:
73
app/web/static/script.js
Normal file
73
app/web/static/script.js
Normal file
@ -0,0 +1,73 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const nodeGridContainer = document.getElementById('node-grid-container');
|
||||
const nodeCountSpan = document.getElementById('node-count');
|
||||
const POLLING_INTERVAL_MS = 3000; // Poll every 3 seconds
|
||||
|
||||
async function fetchNodeData() {
|
||||
try {
|
||||
const response = await fetch('/nodes/status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
renderNodeGrid(data.nodes);
|
||||
} catch (error) {
|
||||
console.error("Error fetching node data:", error);
|
||||
nodeGridContainer.innerHTML = '<p class="loading-message">Error loading node data. Please check server connection.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderNodeGrid(nodes) {
|
||||
nodeGridContainer.innerHTML = ''; // Clear existing nodes
|
||||
nodeCountSpan.textContent = nodes.length; // Update total node count
|
||||
|
||||
if (nodes.length === 0) {
|
||||
nodeGridContainer.innerHTML = '<p class="loading-message">No nodes reporting yet. Start a client!</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeCell = document.createElement('div');
|
||||
nodeCell.classList.add('node-cell');
|
||||
nodeCell.classList.add(`node-${node.health_status}`); // Apply health color class
|
||||
|
||||
// Truncate UUID for display
|
||||
const displayUuid = node.uuid.substring(0, 8) + '...';
|
||||
|
||||
nodeCell.innerHTML = `
|
||||
<div class="node-uuid" title="${node.uuid}">${displayUuid}</div>
|
||||
<div class="node-status-text">Status: ${node.health_status.toUpperCase()}</div>
|
||||
<div class="node-tooltip">
|
||||
<p><strong>UUID:</strong> ${node.uuid}</p>
|
||||
<p><strong>IP:</strong> ${node.ip}</p>
|
||||
<p><strong>Last Seen:</strong> ${new Date(node.last_seen).toLocaleTimeString()}</p>
|
||||
<p><strong>Uptime:</strong> ${node.uptime_seconds ? formatUptime(node.uptime_seconds) : 'N/A'}</p>
|
||||
<p><strong>Load Avg (1m, 5m, 15m):</strong> ${node.load_avg ? node.load_avg.join(', ') : 'N/A'}</p>
|
||||
<p><strong>Memory Usage:</strong> ${node.memory_usage_percent ? node.memory_usage_percent.toFixed(2) + '%' : 'N/A'}</p>
|
||||
</div>
|
||||
`;
|
||||
nodeGridContainer.appendChild(nodeCell);
|
||||
});
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const days = Math.floor(seconds / (3600 * 24));
|
||||
seconds %= (3600 * 24);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
seconds %= 3600;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
let parts = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0) parts.push(`${minutes}m`);
|
||||
if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`); // Ensure at least seconds are shown
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// Initial fetch and then set up polling
|
||||
fetchNodeData();
|
||||
setInterval(fetchNodeData, POLLING_INTERVAL_MS);
|
||||
});
|
156
app/web/static/style.css
Normal file
156
app/web/static/style.css
Normal file
@ -0,0 +1,156 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column; /* Changed to column for header + grid */
|
||||
align-items: center;
|
||||
min-height: 100vh; /* Use min-height to allow content to push body height */
|
||||
margin: 0;
|
||||
background-color: #f4f7f6;
|
||||
color: #333;
|
||||
padding: 20px; /* Add some padding */
|
||||
box-sizing: border-box; /* Include padding in element's total width and height */
|
||||
}
|
||||
|
||||
.header-container {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
width: 90%; /* Adjust width */
|
||||
max-width: 800px; /* Max width for header */
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0b2d48;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #e8e8e8;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#node-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* Responsive grid */
|
||||
gap: 15px; /* Space between grid items */
|
||||
width: 90%; /* Adjust width */
|
||||
max-width: 1200px; /* Max width for grid */
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
grid-column: 1 / -1; /* Span across all columns */
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.node-cell {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.1s ease;
|
||||
cursor: pointer;
|
||||
position: relative; /* For tooltip positioning */
|
||||
overflow: hidden; /* Hide overflow for truncated UUID */
|
||||
}
|
||||
|
||||
.node-cell:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.node-uuid {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* Truncate long UUIDs */
|
||||
}
|
||||
|
||||
.node-status-text {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Health Status Colors */
|
||||
.node-healthy {
|
||||
background-color: #e6ffe6; /* Light green */
|
||||
border-color: #4CAF50; /* Green */
|
||||
}
|
||||
|
||||
.node-warning {
|
||||
background-color: #fffacd; /* Light yellow */
|
||||
border-color: #FFC107; /* Orange */
|
||||
}
|
||||
|
||||
.node-critical {
|
||||
background-color: #ffe6e6; /* Light red */
|
||||
border-color: #F44336; /* Red */
|
||||
}
|
||||
|
||||
.node-unknown {
|
||||
background-color: #f0f0f0; /* Light gray */
|
||||
border-color: #9E9E9E; /* Gray */
|
||||
}
|
||||
|
||||
/* Tooltip styles */
|
||||
.node-tooltip {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 200px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 100%; /* Position above the node cell */
|
||||
left: 50%;
|
||||
margin-left: -100px; /* Center the tooltip */
|
||||
transition: opacity 0.3s;
|
||||
font-size: 0.8rem;
|
||||
white-space: normal; /* Allow text to wrap */
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.node-tooltip::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 100%; /* At the bottom of the tooltip */
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #333 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.node-cell:hover .node-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.node-tooltip p {
|
||||
margin: 2px 0;
|
||||
color: #eee;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Node Monitor</title>
|
||||
<link rel="stylesheet" href="/static/style.css"> <!-- NEW: Link to CSS -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-container">
|
||||
<h1>Distributed Node Monitoring System</h1>
|
||||
<p>Service ID: <code>{{ service_uuid }}</code></p>
|
||||
<p>Total Nodes: <span id="node-count">0</span></p>
|
||||
</div>
|
||||
|
||||
<div id="node-grid-container" class="node-grid">
|
||||
<!-- Node cells will be dynamically inserted here by JavaScript -->
|
||||
<p class="loading-message">Loading node data...</p>
|
||||
</div>
|
||||
|
||||
<script src="/static/script.js"></script> <!-- NEW: Link to JavaScript -->
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user