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:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

View 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>