Files
dashcaddy/status/monitoring-dashboard.html
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

839 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DashCaddy - Monitoring Dashboard</title>
<link rel="icon" href="/assets/dashcaddy-favicon.ico">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 28px;
color: #667eea;
}
.tabs {
display: flex;
gap: 12px;
}
.tab {
padding: 10px 20px;
background: #f0f0f0;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.tab:hover {
background: #e0e0e0;
}
.tab.active {
background: #667eea;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card h2 {
font-size: 18px;
margin-bottom: 16px;
color: #667eea;
display: flex;
align-items: center;
gap: 8px;
}
.stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.stat:last-child {
border-bottom: none;
}
.stat-label {
font-size: 14px;
color: #666;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #333;
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s;
}
.progress-fill.warning {
background: linear-gradient(90deg, #f59e0b, #ef4444);
}
.progress-fill.danger {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-badge.up {
background: #d1fae5;
color: #065f46;
}
.status-badge.down {
background: #fee2e2;
color: #991b1b;
}
.status-badge.warning {
background: #fef3c7;
color: #92400e;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #999;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
}
.loading {
text-align: center;
padding: 40px;
}
.spinner {
border: 3px solid #f0f0f0;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.chart-container {
height: 200px;
margin-top: 16px;
}
.list-item {
padding: 12px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.list-item:last-child {
border-bottom: none;
}
.list-item:hover {
background: #f9fafb;
}
.timestamp {
font-size: 12px;
color: #999;
}
.alert-box {
padding: 12px;
border-radius: 8px;
margin-bottom: 12px;
}
.alert-box.info {
background: #dbeafe;
color: #1e40af;
}
.alert-box.warning {
background: #fef3c7;
color: #92400e;
}
.alert-box.error {
background: #fee2e2;
color: #991b1b;
}
.alert-box.success {
background: #d1fae5;
color: #065f46;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 DashCaddy Monitoring</h1>
<div class="tabs">
<button class="tab active" onclick="switchTab('resources')">📊 Resources</button>
<button class="tab" onclick="switchTab('health')">🏥 Health</button>
<button class="tab" onclick="switchTab('backups')">💾 Backups</button>
<button class="tab" onclick="switchTab('updates')">🔄 Updates</button>
</div>
</div>
<!-- Resources Tab -->
<div id="resources-tab" class="tab-content active">
<div class="card" style="margin-bottom: 24px;">
<h2>📊 Container Resource Monitoring</h2>
<div class="actions">
<button class="btn btn-primary" onclick="refreshResources()">🔄 Refresh</button>
<button class="btn btn-secondary" onclick="configureAlerts()">⚙️ Configure Alerts</button>
</div>
</div>
<div id="resources-grid" class="grid">
<div class="loading">
<div class="spinner"></div>
<p>Loading container stats...</p>
</div>
</div>
</div>
<!-- Health Tab -->
<div id="health-tab" class="tab-content">
<div class="card" style="margin-bottom: 24px;">
<h2>🏥 Service Health Monitoring</h2>
<div class="actions">
<button class="btn btn-primary" onclick="refreshHealth()">🔄 Refresh</button>
<button class="btn btn-secondary" onclick="addHealthCheck()"> Add Health Check</button>
</div>
</div>
<div id="health-grid" class="grid">
<div class="loading">
<div class="spinner"></div>
<p>Loading health status...</p>
</div>
</div>
<div class="card" style="margin-top: 24px;">
<h2>🚨 Open Incidents</h2>
<div id="incidents-list"></div>
</div>
</div>
<!-- Backups Tab -->
<div id="backups-tab" class="tab-content">
<div class="card" style="margin-bottom: 24px;">
<h2>💾 Backup Management</h2>
<div class="actions">
<button class="btn btn-success" onclick="executeBackup()">▶️ Run Backup Now</button>
<button class="btn btn-secondary" onclick="configureBackups()">⚙️ Configure Schedule</button>
<button class="btn btn-primary" onclick="refreshBackups()">🔄 Refresh</button>
</div>
</div>
<div class="card">
<h2>📜 Backup History</h2>
<div id="backup-history"></div>
</div>
</div>
<!-- Updates Tab -->
<div id="updates-tab" class="tab-content">
<div class="card" style="margin-bottom: 24px;">
<h2>🔄 Update Management</h2>
<div class="actions">
<button class="btn btn-primary" onclick="checkUpdates()">🔍 Check for Updates</button>
<button class="btn btn-secondary" onclick="viewUpdateHistory()">📜 View History</button>
</div>
</div>
<div id="updates-grid" class="grid">
<div class="loading">
<div class="spinner"></div>
<p>Checking for updates...</p>
</div>
</div>
</div>
</div>
<script>
const API_BASE = '/api';
// Tab switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(`${tabName}-tab`).classList.add('active');
// Load data for the tab
switch(tabName) {
case 'resources':
loadResources();
break;
case 'health':
loadHealth();
break;
case 'backups':
loadBackups();
break;
case 'updates':
loadUpdates();
break;
}
}
// Resources Tab Functions
async function loadResources() {
try {
const response = await fetch(`${API_BASE}/monitoring/stats`);
const data = await response.json();
if (data.success) {
displayResources(data.stats);
} else {
showError('resources-grid', data.error);
}
} catch (error) {
showError('resources-grid', error.message);
}
}
function displayResources(stats) {
const grid = document.getElementById('resources-grid');
if (Object.keys(stats).length === 0) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📊</div><p>No containers being monitored</p></div>';
return;
}
grid.innerHTML = Object.entries(stats).map(([id, container]) => {
const current = container.current;
if (!current) return '';
const cpuClass = current.cpu.percent > 80 ? 'danger' : current.cpu.percent > 60 ? 'warning' : '';
const memClass = current.memory.percent > 90 ? 'danger' : current.memory.percent > 70 ? 'warning' : '';
return `
<div class="card">
<h2>${container.name}</h2>
<div class="stat">
<span class="stat-label">CPU Usage</span>
<span class="stat-value">${current.cpu.percent.toFixed(1)}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill ${cpuClass}" style="width: ${current.cpu.percent}%"></div>
</div>
<div class="stat">
<span class="stat-label">Memory Usage</span>
<span class="stat-value">${current.memory.usageMB} / ${current.memory.limitMB} MB</span>
</div>
<div class="progress-bar">
<div class="progress-fill ${memClass}" style="width: ${current.memory.percent}%"></div>
</div>
<div class="stat">
<span class="stat-label">Network RX/TX</span>
<span class="stat-value">${current.network.rxMB} / ${current.network.txMB} MB</span>
</div>
<div class="stat">
<span class="stat-label">Disk Read/Write</span>
<span class="stat-value">${current.disk.readMB} / ${current.disk.writeMB} MB</span>
</div>
${container.aggregated ? `
<div class="stat">
<span class="stat-label">24h Avg CPU</span>
<span class="stat-value">${container.aggregated.cpu.avg.toFixed(1)}%</span>
</div>
` : ''}
<div class="actions">
<button class="btn btn-secondary btn-sm" onclick="viewHistory('${id}')">📈 History</button>
<button class="btn btn-secondary btn-sm" onclick="configureAlert('${id}')">⚙️ Alerts</button>
</div>
</div>
`;
}).join('');
}
function refreshResources() {
document.getElementById('resources-grid').innerHTML = '<div class="loading"><div class="spinner"></div><p>Refreshing...</p></div>';
loadResources();
}
// Health Tab Functions
async function loadHealth() {
try {
const [statusRes, incidentsRes] = await Promise.all([
fetch(`${API_BASE}/health-check/status`),
fetch(`${API_BASE}/health-check/incidents`)
]);
const statusData = await statusRes.json();
const incidentsData = await incidentsRes.json();
if (statusData.success) {
displayHealth(statusData.status);
}
if (incidentsData.success) {
displayIncidents(incidentsData.incidents);
}
} catch (error) {
showError('health-grid', error.message);
}
}
function displayHealth(status) {
const grid = document.getElementById('health-grid');
if (Object.keys(status).length === 0) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🏥</div><p>No health checks configured</p></div>';
return;
}
grid.innerHTML = Object.entries(status).map(([id, service]) => `
<div class="card">
<h2>${service.name}</h2>
<div class="stat">
<span class="stat-label">Status</span>
<span class="status-badge ${service.status}">${service.status.toUpperCase()}</span>
</div>
<div class="stat">
<span class="stat-label">Response Time</span>
<span class="stat-value">${service.responseTime}ms</span>
</div>
<div class="stat">
<span class="stat-label">24h Uptime</span>
<span class="stat-value">${service.uptime['24h'].toFixed(2)}%</span>
</div>
<div class="stat">
<span class="stat-label">7d Uptime</span>
<span class="stat-value">${service.uptime['7d'].toFixed(2)}%</span>
</div>
${service.sla ? `
<div class="stat">
<span class="stat-label">SLA Target</span>
<span class="stat-value">${service.sla.target}%</span>
</div>
` : ''}
<div class="actions">
<button class="btn btn-secondary btn-sm" onclick="viewHealthStats('${id}')">📊 Stats</button>
<button class="btn btn-secondary btn-sm" onclick="editHealthCheck('${id}')">✏️ Edit</button>
</div>
</div>
`).join('');
}
function displayIncidents(incidents) {
const list = document.getElementById('incidents-list');
if (incidents.length === 0) {
list.innerHTML = '<div class="empty-state"><p>No open incidents</p></div>';
return;
}
list.innerHTML = incidents.map(incident => `
<div class="list-item">
<div>
<strong>${incident.serviceId}</strong>
<p>${incident.message}</p>
<span class="timestamp">${new Date(incident.createdAt).toLocaleString()}</span>
</div>
<span class="status-badge ${incident.severity}">${incident.severity}</span>
</div>
`).join('');
}
function refreshHealth() {
document.getElementById('health-grid').innerHTML = '<div class="loading"><div class="spinner"></div><p>Refreshing...</p></div>';
loadHealth();
}
// Backups Tab Functions
async function loadBackups() {
try {
const response = await fetch(`${API_BASE}/backups/history`);
const data = await response.json();
if (data.success) {
displayBackups(data.history);
} else {
showError('backup-history', data.error);
}
} catch (error) {
showError('backup-history', error.message);
}
}
function displayBackups(history) {
const container = document.getElementById('backup-history');
if (history.length === 0) {
container.innerHTML = '<div class="empty-state"><p>No backups yet</p></div>';
return;
}
container.innerHTML = history.map(backup => `
<div class="list-item">
<div>
<strong>${backup.name}</strong>
<p>Size: ${(backup.size / 1024 / 1024).toFixed(2)} MB | Duration: ${backup.duration}ms</p>
<span class="timestamp">${new Date(backup.timestamp).toLocaleString()}</span>
</div>
<div style="display: flex; gap: 8px;">
<span class="status-badge ${backup.status}">${backup.status}</span>
${backup.status === 'success' ? `
<button class="btn btn-sm btn-primary" onclick="restoreBackup('${backup.id}')">🔄 Restore</button>
` : ''}
</div>
</div>
`).join('');
}
async function executeBackup() {
if (!confirm('Execute a manual backup now?')) return;
try {
const response = await fetch(`${API_BASE}/backups/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
include: ['all'],
destinations: [{ type: 'local' }]
})
});
const data = await response.json();
if (data.success) {
alert('Backup completed successfully!');
loadBackups();
} else {
alert('Backup failed: ' + data.error);
}
} catch (error) {
alert('Backup failed: ' + error.message);
}
}
async function restoreBackup(backupId) {
if (!confirm('Restore from this backup? This will overwrite current configuration.')) return;
try {
const response = await fetch(`${API_BASE}/backups/restore/${backupId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await response.json();
if (data.success) {
alert('Restore completed successfully!');
location.reload();
} else {
alert('Restore failed: ' + data.error);
}
} catch (error) {
alert('Restore failed: ' + error.message);
}
}
function refreshBackups() {
loadBackups();
}
// Updates Tab Functions
async function loadUpdates() {
try {
const response = await fetch(`${API_BASE}/updates/available`);
const data = await response.json();
if (data.success) {
displayUpdates(data.updates);
} else {
showError('updates-grid', data.error);
}
} catch (error) {
showError('updates-grid', error.message);
}
}
function displayUpdates(updates) {
const grid = document.getElementById('updates-grid');
if (updates.length === 0) {
grid.innerHTML = '<div class="empty-state"><div class="empty-state-icon">✅</div><p>All containers are up to date!</p></div>';
return;
}
grid.innerHTML = updates.map(update => `
<div class="card">
<h2>${update.containerName}</h2>
<div class="stat">
<span class="stat-label">Image</span>
<span class="stat-value">${update.imageName}</span>
</div>
<div class="stat">
<span class="stat-label">Current</span>
<span class="stat-value">${update.currentDigest}</span>
</div>
<div class="stat">
<span class="stat-label">Latest</span>
<span class="stat-value">${update.latestDigest}</span>
</div>
<div class="stat">
<span class="stat-label">Detected</span>
<span class="timestamp">${new Date(update.detectedAt).toLocaleString()}</span>
</div>
<div class="actions">
<button class="btn btn-success" onclick="updateContainer('${update.containerId}')">⬆️ Update Now</button>
<button class="btn btn-secondary" onclick="scheduleUpdate('${update.containerId}')">📅 Schedule</button>
</div>
</div>
`).join('');
}
async function checkUpdates() {
document.getElementById('updates-grid').innerHTML = '<div class="loading"><div class="spinner"></div><p>Checking for updates...</p></div>';
try {
const response = await fetch(`${API_BASE}/updates/check`, { method: 'POST' });
const data = await response.json();
if (data.success) {
displayUpdates(data.updates);
} else {
showError('updates-grid', data.error);
}
} catch (error) {
showError('updates-grid', error.message);
}
}
async function updateContainer(containerId) {
if (!confirm('Update this container? It will be restarted.')) return;
try {
const response = await fetch(`${API_BASE}/updates/update/${containerId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ autoRollback: true })
});
const data = await response.json();
if (data.success) {
alert('Update completed successfully!');
loadUpdates();
} else {
alert('Update failed: ' + data.error);
}
} catch (error) {
alert('Update failed: ' + error.message);
}
}
// Utility Functions
function showError(containerId, message) {
document.getElementById(containerId).innerHTML = `
<div class="alert-box error">
<strong>Error:</strong> ${message}
</div>
`;
}
// Placeholder functions for future implementation
function configureAlerts() {
alert('Alert configuration UI coming soon!');
}
function viewHistory(containerId) {
alert('Historical charts coming soon!');
}
function configureAlert(containerId) {
alert('Alert configuration UI coming soon!');
}
function addHealthCheck() {
alert('Add health check UI coming soon!');
}
function viewHealthStats(serviceId) {
alert('Health statistics UI coming soon!');
}
function editHealthCheck(serviceId) {
alert('Edit health check UI coming soon!');
}
function configureBackups() {
alert('Backup configuration UI coming soon!');
}
function scheduleUpdate(containerId) {
alert('Schedule update UI coming soon!');
}
function viewUpdateHistory() {
alert('Update history UI coming soon!');
}
// Auto-refresh every 30 seconds
setInterval(() => {
const activeTab = document.querySelector('.tab-content.active').id;
switch(activeTab) {
case 'resources-tab':
loadResources();
break;
case 'health-tab':
loadHealth();
break;
}
}, 30000);
// Initial load
loadResources();
</script>
</body>
</html>