Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
838
status/monitoring-dashboard.html
Normal file
838
status/monitoring-dashboard.html
Normal file
@@ -0,0 +1,838 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user