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>
235 lines
13 KiB
JavaScript
235 lines
13 KiB
JavaScript
/**
|
|
* Tractatus Admin Navbar Component
|
|
* Hamburger menu with organized admin page groups
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
class AdminNavbar {
|
|
constructor() {
|
|
this.mobileMenuOpen = false;
|
|
this.adminUser = JSON.parse(localStorage.getItem('admin_user') || '{}');
|
|
this.adminName = this.adminUser.name || this.adminUser.email || 'Admin';
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
const container = document.getElementById('admin-navbar');
|
|
if (!container) return;
|
|
|
|
const pageTitle = container.dataset.pageTitle || 'Admin';
|
|
const pageIcon = container.dataset.pageIcon || 'default';
|
|
|
|
this.render(container, pageTitle, pageIcon);
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
getIcon(iconType) {
|
|
const icons = {
|
|
default: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>',
|
|
calendar: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
|
|
dashboard: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>',
|
|
blog: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>',
|
|
analytics: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>',
|
|
server: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>'
|
|
};
|
|
return icons[iconType] || icons.default;
|
|
}
|
|
|
|
render(container, pageTitle, pageIcon) {
|
|
const iconSvg = this.getIcon(pageIcon);
|
|
|
|
container.innerHTML = `
|
|
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
|
|
<!-- Left: Logo + Page Title -->
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 flex items-center">
|
|
<div class="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
${iconSvg}
|
|
</svg>
|
|
</div>
|
|
<span class="ml-3 text-xl font-bold text-gray-900">${pageTitle}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: User + Hamburger Menu -->
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm text-gray-600 hidden sm:inline">${this.adminName}</span>
|
|
|
|
<button id="admin-menu-btn" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded p-2" aria-label="Toggle admin menu">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin Menu Drawer -->
|
|
<div id="admin-menu" class="hidden fixed inset-0 z-[9999]">
|
|
<!-- Backdrop -->
|
|
<div id="admin-menu-backdrop" class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity"></div>
|
|
|
|
<!-- Menu Panel -->
|
|
<div id="admin-menu-panel" class="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white shadow-2xl transform transition-transform duration-300 ease-out overflow-y-auto">
|
|
<div class="flex justify-between items-center px-5 h-16 border-b border-gray-200 sticky top-0 bg-white z-10">
|
|
<div class="flex items-center space-x-2">
|
|
<div class="h-6 w-6 bg-blue-600 rounded flex items-center justify-center">
|
|
<svg class="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="font-bold text-gray-900">Admin</span>
|
|
</div>
|
|
<button id="admin-menu-close-btn" class="text-gray-600 hover:text-gray-900 p-2 rounded hover:bg-gray-100 transition" aria-label="Close menu">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="p-5 space-y-3">
|
|
<!-- Primary Tools -->
|
|
<div class="pb-3 mb-3 border-b border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Primary Tools</p>
|
|
<a href="/admin/calendar.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
|
|
<span class="text-sm">📅 Task Calendar</span>
|
|
</a>
|
|
<a href="/admin/dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
|
|
<span class="text-sm">📊 Dashboard</span>
|
|
</a>
|
|
<a href="/admin/audit-analytics.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
|
|
<span class="text-sm">📈 Audit Analytics</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Content Management -->
|
|
<div class="pb-3 mb-3 border-b border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Content Management</p>
|
|
<a href="/admin/blog-curation.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📝 Blog Curation</span>
|
|
</a>
|
|
<a href="/admin/editorial-guidelines.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📰 Editorial Guidelines</span>
|
|
</a>
|
|
<a href="/admin/newsletter-management.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📧 Newsletter</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- CRM & Communications -->
|
|
<div class="pb-3 mb-3 border-b border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">CRM & Communications</p>
|
|
<a href="/admin/crm-dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">🎯 CRM Dashboard</span>
|
|
</a>
|
|
<a href="/admin/unified-inbox.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📥 Unified Inbox</span>
|
|
</a>
|
|
<a href="/admin/contact-management.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📬 Contact Management</span>
|
|
</a>
|
|
<a href="/admin/case-moderation.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📋 Case Submissions</span>
|
|
</a>
|
|
<a href="/admin/media-triage.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📰 Media Inquiries</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- System & Framework -->
|
|
<div class="pb-3 mb-3 border-b border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">System & Framework</p>
|
|
<a href="/admin/credential-vault.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">🔐 Credential Vault</span>
|
|
</a>
|
|
<a href="/admin/rule-manager.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">🔧 Rule Manager</span>
|
|
</a>
|
|
<a href="/admin/hooks-dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">🔒 Hooks Dashboard</span>
|
|
</a>
|
|
<a href="/admin/disk-monitoring.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">💾 Disk Monitoring</span>
|
|
</a>
|
|
<a href="/admin/project-manager.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📁 Project Manager</span>
|
|
</a>
|
|
<a href="/admin/claude-md-migrator.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
|
<span class="text-sm">📄 CLAUDE.md Migrator</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Account -->
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Account</p>
|
|
<div class="px-3 py-2.5 text-gray-600 text-sm">
|
|
<div class="font-medium">${this.adminName}</div>
|
|
<div class="text-xs text-gray-500">${this.adminUser.email || ''}</div>
|
|
</div>
|
|
<button id="admin-logout-btn" class="w-full text-left px-3 py-2.5 text-red-600 hover:bg-red-50 rounded-lg transition font-medium">
|
|
<span class="text-sm">🚪 Logout</span>
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
</nav>
|
|
`;
|
|
}
|
|
|
|
attachEventListeners() {
|
|
const menuBtn = document.getElementById('admin-menu-btn');
|
|
const menu = document.getElementById('admin-menu');
|
|
const menuPanel = document.getElementById('admin-menu-panel');
|
|
const closeBtn = document.getElementById('admin-menu-close-btn');
|
|
const backdrop = document.getElementById('admin-menu-backdrop');
|
|
const logoutBtn = document.getElementById('admin-logout-btn');
|
|
|
|
if (menuBtn && menu && menuPanel) {
|
|
menuBtn.addEventListener('click', () => this.openMenu(menu, menuPanel));
|
|
closeBtn.addEventListener('click', () => this.closeMenu(menu, menuPanel));
|
|
backdrop.addEventListener('click', () => this.closeMenu(menu, menuPanel));
|
|
}
|
|
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => {
|
|
localStorage.removeItem('admin_token');
|
|
localStorage.removeItem('admin_user');
|
|
window.location.href = '/admin/login.html';
|
|
});
|
|
}
|
|
}
|
|
|
|
openMenu(menu, panel) {
|
|
menu.classList.remove('hidden');
|
|
// Force reflow
|
|
panel.offsetHeight;
|
|
panel.classList.remove('translate-x-full');
|
|
this.mobileMenuOpen = true;
|
|
}
|
|
|
|
closeMenu(menu, panel) {
|
|
panel.classList.add('translate-x-full');
|
|
setTimeout(() => {
|
|
menu.classList.add('hidden');
|
|
}, 300);
|
|
this.mobileMenuOpen = false;
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => new AdminNavbar());
|
|
} else {
|
|
new AdminNavbar();
|
|
}
|
|
|
|
})();
|