Initial commit: CloudOps infrastructure platform
This commit is contained in:
466
platform/templates/dashboard.html
Normal file
466
platform/templates/dashboard.html
Normal file
@@ -0,0 +1,466 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user