Files
CloudOps/platform/templates/dashboard.html

467 lines
22 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div id="dashboard-page" class="page">
<div class="card">
<div class="card-header">
<div class="card-title"><span>📦</span><span>Application Status</span></div>
<span style="color:var(--gray);" id="container-count">{{ containers|length }} containers</span>
</div>
<div class="containers-grid" id="containers-grid">
{% for container in containers %}
<div class="container-card {% if 'Up' in container.status %}running{% else %}stopped{% endif %}">
<div class="container-name">
<span>{{ container.name }}</span>
<span class="badge {% if 'Up' in container.status %}badge-running{% else %}badge-stopped{% endif %}">
{% if 'Up' in container.status %}Running{% else %}Stopped{% endif %}
</span>
</div>
<div class="container-status">{{ container.status }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card-title"><span>📈</span><span>Quick Stats</span></div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="stat-total">{{ containers|length }}</div>
<div class="stat-label">Total Containers</div>
</div>
<div class="stat-card">
<div class="stat-number" id="stat-running">{{ running_count }}</div>
<div class="stat-label">Running</div>
</div>
<div class="stat-card">
<div class="stat-number" id="stat-local">{{ backups|length }}</div>
<div class="stat-label">Local Backups</div>
</div>
<div class="stat-card">
<div class="stat-number" id="stat-vm">{{ vm_backups|length }}</div>
<div class="stat-label">VM Backups</div>
</div>
</div>
</div>
</div>
<div id="restore-page" class="page" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title"><span>🔄</span><span>Restore Configuration</span></div>
</div>
<div class="restore-form">
<div class="form-section">
<div class="form-section-title">Step 1 — Select Backup Source</div>
<div class="radio-group">
<label class="radio-card">
<input type="radio" name="backup_source" value="local" checked onchange="updateBackupList()">
<div class="radio-body">
<span class="radio-icon">🖥️</span>
<div>
<div class="radio-label">Main Server (local)</div>
<div class="radio-desc">Backups in <code>/root/backups/</code></div>
</div>
</div>
</label>
<label class="radio-card">
<input type="radio" name="backup_source" value="vm" onchange="updateBackupList()">
<div class="radio-body">
<span class="radio-icon">💾</span>
<div>
<div class="radio-label">VM Backup Server</div>
<div class="radio-desc">Backups in <code>/backups/main-server/</code></div>
</div>
</div>
</label>
</div>
<div class="form-group" style="margin-top:14px;">
<label class="form-label">Select Backup File</label>
<select id="backup-file-select" class="form-control">
<optgroup label="Main Server Backups" id="local-options">
{% for b in backups %}
<option value="{{ b }}" data-source="local">{{ b }}</option>
{% else %}
<option disabled>No local backups found</option>
{% endfor %}
</optgroup>
<optgroup label="VM Backups" id="vm-options" style="display:none;">
{% for b in vm_backups %}
<option value="{{ b }}" data-source="vm">{{ b }}</option>
{% else %}
<option disabled>No VM backups found</option>
{% endfor %}
</optgroup>
</select>
</div>
</div>
<div class="form-section">
<div class="form-section-title">Step 2 — Select Restore Target</div>
<div class="radio-group">
<label class="radio-card">
<input type="radio" name="restore_target" value="local" checked onchange="toggleRemoteFields()">
<div class="radio-body">
<span class="radio-icon">🎯</span>
<div>
<div class="radio-label">This Server ({{ main_server }})</div>
<div class="radio-desc">Restore directly on the main server</div>
</div>
</div>
</label>
<label class="radio-card">
<input type="radio" name="restore_target" value="remote" onchange="toggleRemoteFields()">
<div class="radio-body">
<span class="radio-icon">📡</span>
<div>
<div class="radio-label">Remote Machine (via SSH)</div>
<div class="radio-desc">Restore on another server or the VM</div>
</div>
</div>
</label>
</div>
<div id="remote-fields" style="display:none; margin-top:16px;">
<div class="form-row">
<div class="form-group">
<label class="form-label">Target IP / Hostname</label>
<input type="text" id="remote-ip" class="form-control" placeholder="192.168.1.100 or localhost">
</div>
<div class="form-group">
<label class="form-label">SSH Port</label>
<input type="text" id="remote-port" class="form-control" value="22" placeholder="22">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">SSH User</label>
<input type="text" id="remote-user" class="form-control" value="root">
</div>
</div>
<div class="form-group">
<label class="form-label">Authentication Method</label>
<div class="radio-group" style="gap:10px;">
<label class="radio-card small">
<input type="radio" name="auth_method" value="key" checked onchange="toggleAuthFields()">
<div class="radio-body">
<span>🔑</span>
<div class="radio-label">SSH Key</div>
</div>
</label>
<label class="radio-card small">
<input type="radio" name="auth_method" value="password" onchange="toggleAuthFields()">
<div class="radio-body">
<span>🔒</span>
<div class="radio-label">Password</div>
</div>
</label>
</div>
</div>
<div id="key-field" class="form-group">
<label class="form-label">SSH Key Path (on this server)</label>
<input type="text" id="ssh-key-path" class="form-control" value="/root/.ssh/contabo-key" placeholder="/root/.ssh/id_rsa">
</div>
<div id="password-field" class="form-group" style="display:none;">
<label class="form-label">SSH Password</label>
<input type="password" id="ssh-password" class="form-control" placeholder="SSH password">
</div>
</div>
</div>
<div class="form-section" style="border:none; padding-top:0;">
<button class="btn btn-danger btn-lg" onclick="launchRestore()" id="restore-btn">
🚀 Start Restore
</button>
<p style="color:var(--gray); font-size:13px; margin-top:8px;">
⚠️ Restore will skip containers that are already running and healthy.
</p>
</div>
</div>
<div id="restore-log-wrapper" style="display:none; margin-top:20px;">
<div class="card-header" style="margin-bottom:10px;">
<div class="card-title"><span>📋</span><span>Restore Log</span></div>
<span id="restore-status-badge" class="badge badge-running">Running...</span>
</div>
<div id="restore-log" class="log-console"></div>
<div style="color:var(--gray); font-size:12px; margin-top:8px;" id="restore-elapsed"></div>
</div>
</div>
</div>
<div id="backups-page" class="page" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title"><span>💾</span><span>Available Backups</span></div>
<button class="btn btn-secondary" onclick="refreshBackupsList()">🔄 Refresh</button>
</div>
<div class="two-column">
<div>
<h3 style="margin-bottom:12px;">🖥️ Main Server <small style="color:var(--gray); font-size:13px;">/root/backups/</small></h3>
<div class="backup-list" id="local-backup-list">
{% for b in backups %}
<div class="backup-item">
<div class="backup-date">{{ b }}</div>
<button class="btn btn-secondary btn-sm" onclick="quickRestore('local','{{ b }}')">↩ Restore here</button>
</div>
{% else %}
<div class="backup-item">No local backups found</div>
{% endfor %}
</div>
</div>
<div>
<h3 style="margin-bottom:12px;">💾 VM Server <small style="color:var(--gray); font-size:13px;">/backups/main-server/</small></h3>
<div class="backup-list" id="vm-backup-list">
{% for b in vm_backups %}
<div class="backup-item">
<div class="backup-date">{{ b }}</div>
<button class="btn btn-secondary btn-sm" onclick="quickRestore('vm','{{ b }}')">↩ Restore here</button>
</div>
{% else %}
<div class="backup-item">No VM backups found</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div id="settings-page" class="page" style="display:none;">
<div class="card">
<div class="card-header">
<div class="card-title"><span>⚙️</span><span>Platform Settings</span></div>
</div>
<div style="max-width:500px;">
<div class="form-group">
<label class="form-label">Main Server IP</label>
<input type="text" value="{{ main_server }}" readonly class="form-control" style="background:var(--light);">
</div>
<div class="form-group">
<label class="form-label">Platform Host</label>
<input type="text" value="Running on main server" readonly class="form-control" style="background:var(--light);">
</div>
<div class="form-group">
<label class="form-label">VM Backup Path</label>
<input type="text" value="/backups/main-server/" readonly class="form-control" style="background:var(--light);">
</div>
<button class="btn btn-secondary" onclick="refreshAll()">🔄 Refresh All Data</button>
</div>
</div>
</div>
<script>
function updateBackupList() {
const source = document.querySelector('input[name="backup_source"]:checked').value;
const sel = document.getElementById('backup-file-select');
document.getElementById('local-options').style.display = source === 'local' ? '' : 'none';
document.getElementById('vm-options').style.display = source === 'vm' ? '' : 'none';
for (let opt of sel.options) {
if (!opt.disabled && opt.parentElement.style.display !== 'none') {
opt.selected = true;
break;
}
}
}
function toggleRemoteFields() {
const target = document.querySelector('input[name="restore_target"]:checked').value;
document.getElementById('remote-fields').style.display = target === 'remote' ? '' : 'none';
}
function toggleAuthFields() {
const method = document.querySelector('input[name="auth_method"]:checked').value;
document.getElementById('key-field').style.display = method === 'key' ? '' : 'none';
document.getElementById('password-field').style.display = method === 'password' ? '' : 'none';
}
function quickRestore(source, filename) {
showPage('restore');
document.querySelector(`input[name="backup_source"][value="${source}"]`).checked = true;
updateBackupList();
const sel = document.getElementById('backup-file-select');
for (let opt of sel.options) {
if (opt.value === filename) { opt.selected = true; break; }
}
document.querySelector('input[name="restore_target"][value="local"]').checked = true;
toggleRemoteFields();
window.scrollTo(0, 0);
}
let currentJobId = null;
let pollInterval = null;
async function launchRestore() {
const source = document.querySelector('input[name="backup_source"]:checked').value;
const file = document.getElementById('backup-file-select').value;
const target = document.querySelector('input[name="restore_target"]:checked').value;
if (!file || file === 'undefined') {
alert('Please select a backup file.');
return;
}
const payload = {
backup_source: source,
backup_file: file,
target: target,
};
if (target === 'remote') {
payload.remote_ip = document.getElementById('remote-ip').value.trim();
payload.remote_port = document.getElementById('remote-port').value.trim() || '22';
payload.remote_user = document.getElementById('remote-user').value.trim();
payload.auth_method = document.querySelector('input[name="auth_method"]:checked').value;
if (payload.auth_method === 'key') {
payload.ssh_key_path = document.getElementById('ssh-key-path').value.trim();
} else {
payload.ssh_password = document.getElementById('ssh-password').value;
}
if (!payload.remote_ip) {
alert('Please enter the target IP address.');
return;
}
}
if (!confirm(`Start restore of "${file}" → ${target === 'local' ? 'this server' : payload.remote_ip}:${payload.remote_port || 22}?`)) return;
const btn = document.getElementById('restore-btn');
btn.disabled = true;
btn.textContent = '⏳ Starting...';
document.getElementById('restore-log-wrapper').style.display = '';
document.getElementById('restore-log').innerHTML = '';
document.getElementById('restore-status-badge').className = 'badge badge-running';
document.getElementById('restore-status-badge').textContent = 'Running...';
try {
const res = await fetch('/restore/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.error) {
appendLog('❌ ERROR: ' + data.error);
btn.disabled = false;
btn.textContent = '🚀 Start Restore';
return;
}
currentJobId = data.job_id;
pollRestore();
} catch (e) {
appendLog('❌ Request failed: ' + e);
btn.disabled = false;
btn.textContent = '🚀 Start Restore';
}
}
function pollRestore() {
if (pollInterval) clearInterval(pollInterval);
let lastLine = 0;
pollInterval = setInterval(async () => {
if (!currentJobId) return;
try {
const res = await fetch(`/restore/status/${currentJobId}`);
const data = await res.json();
const newLines = data.log.slice(lastLine);
newLines.forEach(l => appendLog(l));
lastLine = data.log.length;
document.getElementById('restore-elapsed').textContent = `⏱ Elapsed: ${data.elapsed}s`;
if (data.status !== 'running') {
clearInterval(pollInterval);
const badge = document.getElementById('restore-status-badge');
if (data.status === 'done') {
badge.className = 'badge badge-running';
badge.textContent = '✅ Done';
badge.style.background = '#22c55e';
} else {
badge.className = 'badge badge-stopped';
badge.textContent = '❌ Error';
}
const btn = document.getElementById('restore-btn');
btn.disabled = false;
btn.textContent = '🚀 Start Restore';
}
} catch (e) { }
}, 1500);
}
function appendLog(line) {
const el = document.getElementById('restore-log');
const div = document.createElement('div');
div.className = 'log-line';
if (line.includes('✅')) div.style.color = '#4ade80';
else if (line.includes('❌') || line.includes('ERROR')) div.style.color = '#f87171';
else if (line.includes('⚠️')) div.style.color = '#fbbf24';
else if (line.includes('📌') || line.includes('Step')) div.style.color = '#60a5fa';
div.textContent = line;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
async function refreshBackupsList() {
const res = await fetch('/api/backups');
const data = await res.json();
const renderList = (items, containerId, source) => {
const el = document.getElementById(containerId);
if (!items.length) { el.innerHTML = '<div class="backup-item">No backups found</div>'; return; }
el.innerHTML = items.map(b => `
<div class="backup-item">
<div class="backup-date">${b}</div>
<button class="btn btn-secondary btn-sm" onclick="quickRestore('${source}','${b}')">↩ Restore here</button>
</div>`).join('');
};
renderList(data.local, 'local-backup-list', 'local');
renderList(data.vm, 'vm-backup-list', 'vm');
document.getElementById('stat-local').textContent = data.local.length;
document.getElementById('stat-vm').textContent = data.vm.length;
const localOpts = document.getElementById('local-options');
const vmOpts = document.getElementById('vm-options');
localOpts.innerHTML = data.local.length ? data.local.map(b => `<option value="${b}" data-source="local">${b}</option>`).join('') : '<option disabled>No local backups found</option>';
vmOpts.innerHTML = data.vm.length ? data.vm.map(b => `<option value="${b}" data-source="vm">${b}</option>`).join('') : '<option disabled>No VM backups found</option>';
}
async function refreshContainers() {
const res = await fetch('/api/containers');
const data = await res.json();
document.getElementById('stat-total').textContent = data.containers.length;
document.getElementById('stat-running').textContent = data.running;
document.getElementById('container-count').textContent = data.containers.length + ' containers';
const grid = document.getElementById('containers-grid');
grid.innerHTML = data.containers.map(c => {
const up = c.status.includes('Up');
return `<div class="container-card ${up ? 'running' : 'stopped'}">
<div class="container-name">
<span>${c.name}</span>
<span class="badge ${up ? 'badge-running' : 'badge-stopped'}">${up ? 'Running' : 'Stopped'}</span>
</div>
<div class="container-status">${c.status}</div>
</div>`;
}).join('');
}
function refreshAll() { refreshContainers(); refreshBackupsList(); }
function showPage(pageId) {
document.querySelectorAll('.nav-item').forEach(nav => nav.classList.remove('active'));
document.querySelectorAll('.page').forEach(p => p.style.display = 'none');
document.getElementById(pageId + '-page').style.display = 'block';
const titles = {dashboard: 'Dashboard Overview', restore: 'Restore Actions', backups: 'Backup Management', settings: 'Platform Settings'};
const titleElement = document.getElementById('page-title');
if (titleElement) titleElement.textContent = titles[pageId];
}
document.addEventListener('DOMContentLoaded', () => {
checkServerStatus();
});
</script>
{% endblock %}