Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
839 lines
23 KiB
HTML
839 lines
23 KiB
HTML
<!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>
|