Major refactoring of the codebase, including restructuring of files and directories, renaming of modules and classes, and improvements to the overall organization and readability of the code. This refactoring aims to enhance maintainability, scalability, and clarity of the codebase while preserving existing functionality. The changes include:
- Restructuring of the project directory into client and server components - Renaming of modules and classes to better reflect their purpose and functionality - Moving common utilities and configurations to a shared location - Updating import statements to reflect the new structure - Adding new documentation files for better clarity on various aspects of the project - Removing deprecated or unused code to streamline the codebase - Ensuring that all existing functionality is preserved and that the codebase remains functional after the refactoring.
This commit is contained in:
@@ -0,0 +1,974 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
{% include 'head.html' %}
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 10px;
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background: #fff;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
margin-right: 20px;
|
||||
text-decoration: none;
|
||||
color: #0066cc;
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav a.active {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.host-card {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.host-card.collapsed .host-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.host-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.host-header:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.host-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
font-size: 1.2em;
|
||||
color: #666;
|
||||
transition: transform 0.2s;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.host-card.collapsed .collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.host-name {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.host-body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.plugin-pills {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.plugin-pill {
|
||||
padding: 4px 12px;
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.plugin-pill:hover {
|
||||
background: #90caf9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plugin-pill.active {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.plugin-content {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plugin-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #fafafa;
|
||||
border-left: 3px solid #2196f3;
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: 0.6em;
|
||||
color: #888;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #999;
|
||||
font-size: 0.75em;
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
border-left: 3px solid #f44336;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 3px;
|
||||
color: #c62828;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.nested-metrics {
|
||||
margin-top: 8px;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.nested-header {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
margin: 8px 0 5px 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.container::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Table styling for interface data */
|
||||
.interface-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 0.85em;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.interface-table thead {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.interface-table th {
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.interface-table th.number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.interface-table td {
|
||||
padding: 6px 10px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.interface-table td.number {
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.interface-table tbody tr:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.interface-table tbody tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.interface-table tbody tr:nth-child(even):hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.interface-name {
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div class="nav">
|
||||
<a href="/live">Live Dashboard</a>
|
||||
<a href="/plugins" class="active">Plugin Metrics</a>
|
||||
<a href="/alerts">Alerts</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>{{ header }}</h1>
|
||||
<p class="subtitle">Real-time system metrics from monitoring plugins</p>
|
||||
|
||||
{% if not hosts %}
|
||||
<div class="no-data">
|
||||
<p>No hosts with plugin data available</p>
|
||||
<p style="font-size: 0.9em; margin-top: 10px;">Hosts will appear here once they start sending plugin metrics</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="hosts-container">
|
||||
{% for host in hosts %}
|
||||
<div class="host-card" data-hostname="{{ host.name }}">
|
||||
<div class="host-header" onclick="toggleHost('{{ host.name }}')">
|
||||
<div class="host-title">
|
||||
<span class="collapse-icon">▼</span>
|
||||
<span class="host-name">{{ host.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="host-body">
|
||||
<div class="plugin-pills">
|
||||
{% for plugin in host.plugins %}
|
||||
<div class="plugin-pill" data-plugin="{{ plugin }}" onclick="event.stopPropagation(); showPlugin('{{ host.name }}', '{{ plugin }}')">
|
||||
{{ plugin }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for plugin in host.plugins %}
|
||||
<div class="plugin-content" id="{{ host.name }}-{{ plugin }}" data-hostname="{{ host.name }}" data-plugin="{{ plugin }}">
|
||||
<div class="loading">Loading {{ plugin }} data...</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Track selected plugins per host
|
||||
const selectedPlugins = {};
|
||||
|
||||
function toggleHost(hostname) {
|
||||
const card = document.querySelector(`[data-hostname="${hostname}"]`);
|
||||
card.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
function showPlugin(hostname, pluginName) {
|
||||
// Update selectedPlugins tracker
|
||||
selectedPlugins[hostname] = pluginName;
|
||||
|
||||
// Update active pill
|
||||
const hostCard = document.querySelector(`[data-hostname="${hostname}"]`);
|
||||
hostCard.querySelectorAll('.plugin-pill').forEach(pill => {
|
||||
pill.classList.remove('active');
|
||||
});
|
||||
hostCard.querySelector(`[data-plugin="${pluginName}"]`).classList.add('active');
|
||||
|
||||
// Show plugin content
|
||||
hostCard.querySelectorAll('.plugin-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
const contentDiv = document.getElementById(`${hostname}-${pluginName}`);
|
||||
contentDiv.classList.add('active');
|
||||
|
||||
// Load data if not already loaded
|
||||
if (contentDiv.querySelector('.loading')) {
|
||||
loadPluginData(hostname, pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPluginData(hostname, pluginName) {
|
||||
const contentDiv = document.getElementById(`${hostname}-${pluginName}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/0/hosts/${hostname}/plugins/${pluginName}?limit=1`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.samples && data.samples.length > 0) {
|
||||
const sample = data.samples[0];
|
||||
contentDiv.innerHTML = renderPluginData(sample.data, sample.timestamp);
|
||||
} else {
|
||||
contentDiv.innerHTML = '<div class="no-data">No data available for this plugin</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
contentDiv.innerHTML = `<div class="error">Failed to load plugin data: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPluginData(data, timestamp) {
|
||||
let html = '<div class="metric-grid">';
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// Skip nested objects for now, handle them separately
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
html += renderMetric(key, value);
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Handle nested objects (like partitions in disk_monitor)
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// Check if this is interface data - render as table
|
||||
if (isInterfaceData(key, value)) {
|
||||
html += renderInterfaceTable(key, value);
|
||||
} else if (isInterfaceStatsData(key, value)) {
|
||||
html += renderInterfaceStatsTable(key, value);
|
||||
} else if (isDiskPartitionData(key, value)) {
|
||||
html += renderPartitionTable(key, value);
|
||||
} else if (isDiskIOData(key, value)) {
|
||||
html += renderDiskIOTable(key, value);
|
||||
} else if (isFilesystemData(key, value)) {
|
||||
html += renderFilesystemTable(key, value);
|
||||
} else {
|
||||
// Regular nested metrics display
|
||||
html += `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">📊 ${formatLabel(key)}</div>`;
|
||||
html += '<div class="metric-grid">';
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Handle arrays - more compact display
|
||||
value.forEach((item, idx) => {
|
||||
if (typeof item === 'object') {
|
||||
// Add a compact separator for array items
|
||||
if (idx > 0) {
|
||||
html += `<div style="grid-column: 1/-1; border-top: 1px dashed #ddd; margin: 5px 0;"></div>`;
|
||||
}
|
||||
for (const [subKey, subValue] of Object.entries(item)) {
|
||||
html += renderMetric(`${subKey}`, subValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Handle nested objects
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
if (typeof subValue === 'object') {
|
||||
// Another level of nesting - keep compact
|
||||
html += `<div style="grid-column: 1/-1; margin-top: 5px; font-size: 0.85em; color: #666;"><strong>${subKey}:</strong></div>`;
|
||||
for (const [deepKey, deepValue] of Object.entries(subValue)) {
|
||||
html += renderMetric(deepKey, deepValue);
|
||||
}
|
||||
} else {
|
||||
html += renderMetric(subKey, subValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
html += `<div class="timestamp">Last updated: ${date.toLocaleString()}</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function isInterfaceData(key, value) {
|
||||
// Check if this is interface/network stats data (I/O counters)
|
||||
if (key.toLowerCase().includes('interface') && !key.toLowerCase().includes('interface_stats')) {
|
||||
// Verify it's an object with interface-like structure
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
// Check if values are objects with byte/packet counters
|
||||
const firstKey = Object.keys(value)[0];
|
||||
if (firstKey && typeof value[firstKey] === 'object') {
|
||||
const sample = value[firstKey];
|
||||
return sample.hasOwnProperty('bytes_sent') ||
|
||||
sample.hasOwnProperty('bytes_recv') ||
|
||||
sample.hasOwnProperty('packets_sent') ||
|
||||
sample.hasOwnProperty('tx_bytes') ||
|
||||
sample.hasOwnProperty('rx_bytes');
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isInterfaceStatsData(key, value) {
|
||||
// Check if this is interface stats data (status, speed, mtu, duplex)
|
||||
if (key.toLowerCase() === 'interface_stats' || key.toLowerCase().includes('if_stats')) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const firstKey = Object.keys(value)[0];
|
||||
if (firstKey && typeof value[firstKey] === 'object') {
|
||||
const sample = value[firstKey];
|
||||
return sample.hasOwnProperty('isup') ||
|
||||
sample.hasOwnProperty('speed') ||
|
||||
sample.hasOwnProperty('mtu') ||
|
||||
sample.hasOwnProperty('duplex');
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDiskPartitionData(key, value) {
|
||||
// Check if this is disk partition data
|
||||
if (key.toLowerCase() === 'partitions' || key.toLowerCase().includes('partition')) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const firstKey = Object.keys(value)[0];
|
||||
if (firstKey && typeof value[firstKey] === 'object') {
|
||||
const sample = value[firstKey];
|
||||
return sample.hasOwnProperty('total') &&
|
||||
sample.hasOwnProperty('used') &&
|
||||
sample.hasOwnProperty('free') &&
|
||||
sample.hasOwnProperty('percent');
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDiskIOData(key, value) {
|
||||
// Check if this is disk I/O counter data
|
||||
if (key.toLowerCase().includes('io_counter') || key.toLowerCase().includes('disk_io')) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const firstKey = Object.keys(value)[0];
|
||||
if (firstKey && typeof value[firstKey] === 'object') {
|
||||
const sample = value[firstKey];
|
||||
return sample.hasOwnProperty('read_bytes') ||
|
||||
sample.hasOwnProperty('write_bytes') ||
|
||||
sample.hasOwnProperty('read_count') ||
|
||||
sample.hasOwnProperty('write_count');
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isFilesystemData(key, value) {
|
||||
// Check if this is filesystem info data (from filesystem_info plugin)
|
||||
if (key.toLowerCase() === 'filesystems' && Array.isArray(value)) {
|
||||
if (value.length > 0 && typeof value[0] === 'object') {
|
||||
const sample = value[0];
|
||||
return sample.hasOwnProperty('device') &&
|
||||
sample.hasOwnProperty('mountpoint') &&
|
||||
sample.hasOwnProperty('fstype');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderInterfaceTable(key, interfaces) {
|
||||
let html = `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">🌐 ${formatLabel(key)}</div>`;
|
||||
html += '<table class="interface-table">';
|
||||
|
||||
// Determine columns based on available data
|
||||
const sampleInterface = Object.values(interfaces)[0];
|
||||
const hasBytes = sampleInterface.hasOwnProperty('bytes_sent') || sampleInterface.hasOwnProperty('tx_bytes');
|
||||
const hasPackets = sampleInterface.hasOwnProperty('packets_sent') || sampleInterface.hasOwnProperty('tx_packets');
|
||||
const hasErrors = sampleInterface.hasOwnProperty('errin') || sampleInterface.hasOwnProperty('rx_errors');
|
||||
const hasDrops = sampleInterface.hasOwnProperty('dropin') || sampleInterface.hasOwnProperty('rx_dropped');
|
||||
const hasDelta = sampleInterface.hasOwnProperty('bytes_sent_delta');
|
||||
|
||||
// Build table header
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Interface</th>';
|
||||
|
||||
if (hasBytes) {
|
||||
html += '<th class="number">Bytes Sent</th>';
|
||||
html += '<th class="number">Bytes Recv</th>';
|
||||
if (hasDelta) {
|
||||
html += '<th class="number">Δ Sent</th>';
|
||||
html += '<th class="number">Δ Recv</th>';
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPackets) {
|
||||
html += '<th class="number">Pkts Sent</th>';
|
||||
html += '<th class="number">Pkts Recv</th>';
|
||||
if (hasDelta) {
|
||||
html += '<th class="number">Δ Pkts Sent</th>';
|
||||
html += '<th class="number">Δ Pkts Recv</th>';
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
html += '<th class="number">Errors In</th>';
|
||||
html += '<th class="number">Errors Out</th>';
|
||||
}
|
||||
|
||||
if (hasDrops) {
|
||||
html += '<th class="number">Drops In</th>';
|
||||
html += '<th class="number">Drops Out</th>';
|
||||
}
|
||||
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Build table body
|
||||
html += '<tbody>';
|
||||
for (const [ifName, ifData] of Object.entries(interfaces)) {
|
||||
html += '<tr>';
|
||||
html += `<td class="interface-name">${ifName}</td>`;
|
||||
|
||||
if (hasBytes) {
|
||||
html += `<td class="number">${formatBytes(ifData.bytes_sent || ifData.tx_bytes || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(ifData.bytes_recv || ifData.rx_bytes || 0)}</td>`;
|
||||
if (hasDelta) {
|
||||
html += `<td class="number">${formatBytes(ifData.bytes_sent_delta || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(ifData.bytes_recv_delta || 0)}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPackets) {
|
||||
html += `<td class="number">${(ifData.packets_sent || ifData.tx_packets || 0).toLocaleString()}</td>`;
|
||||
html += `<td class="number">${(ifData.packets_recv || ifData.rx_packets || 0).toLocaleString()}</td>`;
|
||||
if (hasDelta) {
|
||||
html += `<td class="number">${(ifData.packets_sent_delta || 0).toLocaleString()}</td>`;
|
||||
html += `<td class="number">${(ifData.packets_recv_delta || 0).toLocaleString()}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors) {
|
||||
html += `<td class="number">${ifData.errin || ifData.rx_errors || 0}</td>`;
|
||||
html += `<td class="number">${ifData.errout || ifData.tx_errors || 0}</td>`;
|
||||
}
|
||||
|
||||
if (hasDrops) {
|
||||
html += `<td class="number">${ifData.dropin || ifData.rx_dropped || 0}</td>`;
|
||||
html += `<td class="number">${ifData.dropout || ifData.tx_dropped || 0}</td>`;
|
||||
}
|
||||
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderInterfaceStatsTable(key, interfaces) {
|
||||
let html = `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">🔌 ${formatLabel(key)}</div>`;
|
||||
html += '<table class="interface-table">';
|
||||
|
||||
// Table header
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Interface</th>';
|
||||
html += '<th>Status</th>';
|
||||
html += '<th class="number">Speed</th>';
|
||||
html += '<th>Duplex</th>';
|
||||
html += '<th class="number">MTU</th>';
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Table body
|
||||
html += '<tbody>';
|
||||
for (const [ifName, ifData] of Object.entries(interfaces)) {
|
||||
html += '<tr>';
|
||||
html += `<td class="interface-name">${ifName}</td>`;
|
||||
|
||||
// Status with color coding
|
||||
const isUp = ifData.isup;
|
||||
const statusColor = isUp ? '#4caf50' : '#f44336';
|
||||
const statusIcon = isUp ? '✓' : '✗';
|
||||
const statusText = isUp ? 'UP' : 'DOWN';
|
||||
html += `<td style="color: ${statusColor}; font-weight: bold;">${statusIcon} ${statusText}</td>`;
|
||||
|
||||
// Speed
|
||||
const speed = ifData.speed || 0;
|
||||
let speedText = '-';
|
||||
if (speed > 0) {
|
||||
if (speed >= 1000) {
|
||||
speedText = (speed / 1000).toFixed(1) + ' Gbps';
|
||||
} else {
|
||||
speedText = speed + ' Mbps';
|
||||
}
|
||||
}
|
||||
html += `<td class="number">${speedText}</td>`;
|
||||
|
||||
// Duplex
|
||||
let duplexText = ifData.duplex || '-';
|
||||
if (duplexText.includes('NicDuplex.')) {
|
||||
duplexText = duplexText.replace('NicDuplex.', '');
|
||||
}
|
||||
if (duplexText === '2') duplexText = 'FULL';
|
||||
if (duplexText === '1') duplexText = 'HALF';
|
||||
if (duplexText === '0') duplexText = 'UNKNOWN';
|
||||
html += `<td>${duplexText}</td>`;
|
||||
|
||||
// MTU
|
||||
html += `<td class="number">${ifData.mtu || '-'}</td>`;
|
||||
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPartitionTable(key, partitions) {
|
||||
let html = `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">💾 ${formatLabel(key)}</div>`;
|
||||
html += '<table class="interface-table">';
|
||||
|
||||
// Table header
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Mount Point</th>';
|
||||
html += '<th>Device</th>';
|
||||
html += '<th>Type</th>';
|
||||
html += '<th class="number">Total</th>';
|
||||
html += '<th class="number">Used</th>';
|
||||
html += '<th class="number">Free</th>';
|
||||
html += '<th class="number">Use %</th>';
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Table body
|
||||
html += '<tbody>';
|
||||
for (const [mountPoint, partData] of Object.entries(partitions)) {
|
||||
html += '<tr>';
|
||||
html += `<td class="interface-name">${mountPoint}</td>`;
|
||||
html += `<td>${partData.device || '-'}</td>`;
|
||||
html += `<td>${partData.fstype || '-'}</td>`;
|
||||
html += `<td class="number">${formatBytes(partData.total || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(partData.used || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(partData.free || 0)}</td>`;
|
||||
|
||||
// Color code the percentage
|
||||
const percent = partData.percent || 0;
|
||||
let percentColor = '#4caf50'; // green
|
||||
if (percent > 90) percentColor = '#f44336'; // red
|
||||
else if (percent > 75) percentColor = '#ff9800'; // orange
|
||||
else if (percent > 50) percentColor = '#ffc107'; // yellow
|
||||
|
||||
html += `<td class="number" style="color: ${percentColor}; font-weight: bold;">${percent.toFixed(1)}%</td>`;
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderDiskIOTable(key, disks) {
|
||||
let html = `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">📈 ${formatLabel(key)}</div>`;
|
||||
html += '<table class="interface-table">';
|
||||
|
||||
// Determine columns based on available data
|
||||
const sampleDisk = Object.values(disks)[0];
|
||||
const hasDeltas = sampleDisk.hasOwnProperty('read_bytes_delta');
|
||||
const hasTime = sampleDisk.hasOwnProperty('read_time');
|
||||
|
||||
// Table header
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Disk</th>';
|
||||
html += '<th class="number">Read Bytes</th>';
|
||||
html += '<th class="number">Write Bytes</th>';
|
||||
|
||||
if (hasDeltas) {
|
||||
html += '<th class="number">Δ Read</th>';
|
||||
html += '<th class="number">Δ Write</th>';
|
||||
}
|
||||
|
||||
html += '<th class="number">Read Count</th>';
|
||||
html += '<th class="number">Write Count</th>';
|
||||
|
||||
if (hasDeltas) {
|
||||
html += '<th class="number">Δ Reads</th>';
|
||||
html += '<th class="number">Δ Writes</th>';
|
||||
}
|
||||
|
||||
if (hasTime) {
|
||||
html += '<th class="number">Read Time (ms)</th>';
|
||||
html += '<th class="number">Write Time (ms)</th>';
|
||||
}
|
||||
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Table body
|
||||
html += '<tbody>';
|
||||
for (const [diskName, diskData] of Object.entries(disks)) {
|
||||
html += '<tr>';
|
||||
html += `<td class="interface-name">${diskName}</td>`;
|
||||
html += `<td class="number">${formatBytes(diskData.read_bytes || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(diskData.write_bytes || 0)}</td>`;
|
||||
|
||||
if (hasDeltas) {
|
||||
html += `<td class="number">${formatBytes(diskData.read_bytes_delta || 0)}</td>`;
|
||||
html += `<td class="number">${formatBytes(diskData.write_bytes_delta || 0)}</td>`;
|
||||
}
|
||||
|
||||
html += `<td class="number">${(diskData.read_count || 0).toLocaleString()}</td>`;
|
||||
html += `<td class="number">${(diskData.write_count || 0).toLocaleString()}</td>`;
|
||||
|
||||
if (hasDeltas) {
|
||||
html += `<td class="number">${(diskData.read_count_delta || 0).toLocaleString()}</td>`;
|
||||
html += `<td class="number">${(diskData.write_count_delta || 0).toLocaleString()}</td>`;
|
||||
}
|
||||
|
||||
if (hasTime) {
|
||||
html += `<td class="number">${(diskData.read_time || 0).toLocaleString()}</td>`;
|
||||
html += `<td class="number">${(diskData.write_time || 0).toLocaleString()}</td>`;
|
||||
}
|
||||
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderFilesystemTable(key, filesystems) {
|
||||
let html = `<div class="nested-metrics">`;
|
||||
html += `<div class="nested-header">🗄️ ${formatLabel(key)}</div>`;
|
||||
html += '<table class="interface-table">';
|
||||
|
||||
// Table header
|
||||
html += '<thead><tr>';
|
||||
html += '<th>Device</th>';
|
||||
html += '<th>Mount Point</th>';
|
||||
html += '<th>Type</th>';
|
||||
html += '<th>Options</th>';
|
||||
html += '<th class="number">Max File</th>';
|
||||
html += '<th class="number">Max Path</th>';
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Table body
|
||||
html += '<tbody>';
|
||||
for (const fs of filesystems) {
|
||||
html += '<tr>';
|
||||
html += `<td class="interface-name">${fs.device || '-'}</td>`;
|
||||
html += `<td>${fs.mountpoint || '-'}</td>`;
|
||||
html += `<td>${fs.fstype || '-'}</td>`;
|
||||
|
||||
// Format mount options - truncate if too long
|
||||
let opts = fs.opts || '-';
|
||||
if (opts.length > 40) {
|
||||
opts = opts.substring(0, 37) + '...';
|
||||
}
|
||||
html += `<td style="font-size: 0.85em;">${opts}</td>`;
|
||||
|
||||
html += `<td class="number">${fs.maxfile || '-'}</td>`;
|
||||
html += `<td class="number">${fs.maxpath || '-'}</td>`;
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
|
||||
html += '</table>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
return (bytes / 1073741824).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function renderMetric(key, value) {
|
||||
const label = formatLabel(key);
|
||||
const formattedValue = formatValue(key, value);
|
||||
const unit = getUnit(key);
|
||||
|
||||
return `
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">${label}</div>
|
||||
<div class="metric-value">
|
||||
${formattedValue}
|
||||
${unit ? `<span class="metric-unit">${unit}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function formatLabel(key) {
|
||||
return key
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
|
||||
function formatValue(key, value) {
|
||||
if (typeof value === 'number') {
|
||||
// Format percentages
|
||||
if (key.includes('percent') || key.includes('usage')) {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
// Format bytes to MB/GB
|
||||
if (key.includes('bytes') || key.includes('_mb') || key.includes('_gb')) {
|
||||
if (value > 1073741824) {
|
||||
return (value / 1073741824).toFixed(2);
|
||||
} else if (value > 1048576) {
|
||||
return (value / 1048576).toFixed(2);
|
||||
}
|
||||
}
|
||||
// Default number formatting
|
||||
if (value > 1000) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
return value.toFixed(2);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getUnit(key) {
|
||||
if (key.includes('percent') || key.includes('usage')) return '%';
|
||||
if (key.includes('_gb')) return 'GB';
|
||||
if (key.includes('_mb')) return 'MB';
|
||||
if (key.includes('bytes') && !key.includes('_mb') && !key.includes('_gb')) {
|
||||
// Determine unit based on typical size
|
||||
return 'bytes';
|
||||
}
|
||||
if (key.includes('count')) return '';
|
||||
if (key.includes('mhz')) return 'MHz';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Auto-refresh data every 30 seconds
|
||||
setInterval(() => {
|
||||
for (const [hostname, pluginName] of Object.entries(selectedPlugins)) {
|
||||
const contentDiv = document.getElementById(`${hostname}-${pluginName}`);
|
||||
if (contentDiv && contentDiv.classList.contains('active')) {
|
||||
loadPluginData(hostname, pluginName);
|
||||
}
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Initialize by selecting first plugin for each host
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const hostCards = document.querySelectorAll('.host-card');
|
||||
|
||||
hostCards.forEach((card, index) => {
|
||||
const hostname = card.dataset.hostname;
|
||||
const firstPlugin = card.querySelector('.plugin-pill');
|
||||
|
||||
if (firstPlugin) {
|
||||
const pluginName = firstPlugin.dataset.plugin;
|
||||
showPlugin(hostname, pluginName);
|
||||
}
|
||||
|
||||
// Collapse all hosts except the first one
|
||||
if (index > 0) {
|
||||
card.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user