Add comprehensive disk monitoring with real-time metrics: - Backend API endpoints for disk/memory metrics (local + remote) - Admin UI page with CSP-compliant DOM rendering - Health status indicators with color-coded thresholds - SSH-based remote metrics collection from OVH VPS - Auto-refresh every 5 minutes Backend: - src/models/DiskMetrics.model.js: Metrics collection model - src/controllers/diskMetrics.controller.js: 3 admin endpoints - src/routes/diskMetrics.routes.js: Admin-authenticated routes - src/routes/index.js: Register disk-metrics routes Frontend: - public/admin/disk-monitoring.html: Admin dashboard page - public/js/admin-disk-monitoring.js: CSP-compliant UI rendering - public/js/components/navbar-admin.js: Add disk monitoring link Documentation: - deployment-quickstart/UPTIME_MONITORING_SETUP.md API endpoints: - GET /api/admin/disk-metrics (all systems) - GET /api/admin/disk-metrics/local (dev system) - GET /api/admin/disk-metrics/remote (production VPS) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
213 lines
6.2 KiB
JavaScript
213 lines
6.2 KiB
JavaScript
// Disk Monitoring - Admin UI
|
|
// CSP-compliant implementation using DOM manipulation
|
|
|
|
async function loadMetrics() {
|
|
const loading = document.getElementById('loading');
|
|
const metricsContainer = document.getElementById('metrics-container');
|
|
const errorDiv = document.getElementById('error');
|
|
|
|
try {
|
|
loading.classList.remove('hidden');
|
|
metricsContainer.classList.add('hidden');
|
|
errorDiv.classList.add('hidden');
|
|
|
|
const token = localStorage.getItem('token');
|
|
const response = await fetch('/api/admin/disk-metrics', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch metrics: ' + response.status);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Unknown error');
|
|
}
|
|
|
|
renderMetrics(result.data);
|
|
|
|
loading.classList.add('hidden');
|
|
metricsContainer.classList.remove('hidden');
|
|
|
|
} catch (err) {
|
|
console.error('Load metrics error:', err);
|
|
loading.classList.add('hidden');
|
|
errorDiv.classList.remove('hidden');
|
|
|
|
const errorMessage = document.getElementById('error-message');
|
|
errorMessage.textContent = err.message || 'An unexpected error occurred';
|
|
}
|
|
}
|
|
|
|
function renderMetrics(data) {
|
|
const localContainer = document.getElementById('local-metrics');
|
|
const remoteContainer = document.getElementById('remote-metrics');
|
|
|
|
// Clear existing content
|
|
localContainer.textContent = '';
|
|
remoteContainer.textContent = '';
|
|
|
|
// Render local metrics
|
|
if (data.local) {
|
|
renderSystemMetrics(localContainer, data.local, 'Local Development');
|
|
} else {
|
|
renderError(localContainer, 'Local metrics unavailable');
|
|
}
|
|
|
|
// Render remote metrics
|
|
if (data.remote) {
|
|
renderSystemMetrics(remoteContainer, data.remote, 'Production VPS');
|
|
} else {
|
|
renderError(remoteContainer, 'Remote metrics unavailable');
|
|
}
|
|
}
|
|
|
|
function renderSystemMetrics(container, metrics, label) {
|
|
// Disk Usage Card
|
|
const diskCard = createMetricCard(
|
|
'Disk Usage',
|
|
metrics.health,
|
|
[
|
|
{ label: 'Total', value: metrics.total },
|
|
{ label: 'Used', value: metrics.used },
|
|
{ label: 'Available', value: metrics.available },
|
|
{ label: 'Usage', value: metrics.usedPercent + '%', progress: metrics.usedPercent }
|
|
]
|
|
);
|
|
container.appendChild(diskCard);
|
|
|
|
// Memory Card
|
|
if (metrics.memory) {
|
|
const memoryCard = createMetricCard(
|
|
'Memory',
|
|
{ level: metrics.memory.usedPercent >= 90 ? 'critical' : metrics.memory.usedPercent >= 80 ? 'warning' : 'healthy' },
|
|
[
|
|
{ label: 'Total', value: metrics.memory.total },
|
|
{ label: 'Used', value: metrics.memory.usedPercent + '%', progress: metrics.memory.usedPercent }
|
|
]
|
|
);
|
|
container.appendChild(memoryCard);
|
|
}
|
|
|
|
// System Info Card
|
|
const sysCard = createMetricCard(
|
|
'System Info',
|
|
null,
|
|
[
|
|
{ label: 'Hostname', value: metrics.hostname || 'Unknown' },
|
|
{ label: 'Platform', value: metrics.platform || 'Unknown' },
|
|
{ label: 'Uptime', value: (metrics.uptime || 0) + ' hours' }
|
|
]
|
|
);
|
|
container.appendChild(sysCard);
|
|
|
|
// Docker Volumes (if present)
|
|
if (metrics.docker) {
|
|
const dockerCard = createMetricCard(
|
|
'Docker Volumes',
|
|
null,
|
|
[
|
|
{ label: 'Total', value: metrics.docker.total },
|
|
{ label: 'Used', value: metrics.docker.used }
|
|
]
|
|
);
|
|
container.appendChild(dockerCard);
|
|
}
|
|
}
|
|
|
|
function createMetricCard(title, health, items) {
|
|
const card = document.createElement('div');
|
|
card.className = 'metric-card bg-white rounded-lg shadow-lg p-6';
|
|
|
|
// Card header
|
|
const header = document.createElement('div');
|
|
header.className = 'flex items-center justify-between mb-4';
|
|
|
|
const titleEl = document.createElement('h3');
|
|
titleEl.className = 'text-lg font-semibold text-gray-900';
|
|
titleEl.textContent = title;
|
|
header.appendChild(titleEl);
|
|
|
|
// Health indicator (if present)
|
|
if (health) {
|
|
const indicator = document.createElement('span');
|
|
indicator.className = 'health-indicator health-' + health.level;
|
|
indicator.title = health.level.charAt(0).toUpperCase() + health.level.slice(1);
|
|
header.appendChild(indicator);
|
|
}
|
|
|
|
card.appendChild(header);
|
|
|
|
// Card content
|
|
items.forEach(item => {
|
|
const row = document.createElement('div');
|
|
row.className = 'mb-3';
|
|
|
|
const labelDiv = document.createElement('div');
|
|
labelDiv.className = 'flex justify-between text-sm mb-1';
|
|
|
|
const labelSpan = document.createElement('span');
|
|
labelSpan.className = 'text-gray-600';
|
|
labelSpan.textContent = item.label;
|
|
labelDiv.appendChild(labelSpan);
|
|
|
|
const valueSpan = document.createElement('span');
|
|
valueSpan.className = 'font-semibold text-gray-900';
|
|
valueSpan.textContent = item.value;
|
|
labelDiv.appendChild(valueSpan);
|
|
|
|
row.appendChild(labelDiv);
|
|
|
|
// Progress bar (if present)
|
|
if (item.progress !== undefined) {
|
|
const progressBg = document.createElement('div');
|
|
progressBg.className = 'w-full bg-gray-200 rounded-full h-2';
|
|
|
|
const progressBar = document.createElement('div');
|
|
progressBar.className = 'progress-bar h-2 rounded-full ' + getProgressColor(item.progress);
|
|
progressBar.style.width = item.progress + '%';
|
|
|
|
progressBg.appendChild(progressBar);
|
|
row.appendChild(progressBg);
|
|
}
|
|
|
|
card.appendChild(row);
|
|
});
|
|
|
|
return card;
|
|
}
|
|
|
|
function renderError(container, message) {
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'col-span-3 bg-yellow-50 border-l-4 border-yellow-500 p-4 rounded';
|
|
|
|
const errorText = document.createElement('p');
|
|
errorText.className = 'text-yellow-800';
|
|
errorText.textContent = '⚠️ ' + message;
|
|
|
|
errorDiv.appendChild(errorText);
|
|
container.appendChild(errorDiv);
|
|
}
|
|
|
|
function getProgressColor(percent) {
|
|
if (percent >= 90) return 'bg-red-600';
|
|
if (percent >= 80) return 'bg-orange-500';
|
|
if (percent >= 70) return 'bg-yellow-500';
|
|
return 'bg-green-600';
|
|
}
|
|
|
|
// Refresh functionality
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadMetrics();
|
|
|
|
// Refresh button
|
|
const refreshBtn = document.getElementById('refresh-btn');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', loadMetrics);
|
|
}
|
|
|
|
// Auto-refresh every 5 minutes
|
|
setInterval(loadMetrics, 5 * 60 * 1000);
|
|
});
|