279 lines
9.9 KiB
HTML
279 lines
9.9 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en" data-bs-theme="light">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
|
<title>BackupBC{% block title %}{% endblock %}</title>
|
||
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||
|
|
<style>
|
||
|
|
:root { --sidebar-bg: #0f172a; --sidebar-width: 230px; }
|
||
|
|
body { background: #f1f5f9; }
|
||
|
|
|
||
|
|
.sidebar {
|
||
|
|
width: var(--sidebar-width);
|
||
|
|
min-height: 100vh;
|
||
|
|
background: var(--sidebar-bg);
|
||
|
|
position: fixed;
|
||
|
|
top: 0; left: 0;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
padding: 1.5rem 1rem;
|
||
|
|
z-index: 100;
|
||
|
|
}
|
||
|
|
.sidebar-brand {
|
||
|
|
color: #fff;
|
||
|
|
font-weight: 700;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
text-decoration: none;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: .5rem;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
padding-bottom: 1rem;
|
||
|
|
border-bottom: 1px solid rgba(255,255,255,.1);
|
||
|
|
}
|
||
|
|
.sidebar-brand .brand-icon {
|
||
|
|
width: 32px; height: 32px;
|
||
|
|
background: #6366f1;
|
||
|
|
border-radius: 8px;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: .9rem;
|
||
|
|
}
|
||
|
|
.sidebar .nav-link {
|
||
|
|
color: rgba(255,255,255,.55);
|
||
|
|
padding: .5rem .75rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
font-size: .875rem;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: .6rem;
|
||
|
|
transition: all .15s;
|
||
|
|
}
|
||
|
|
.sidebar .nav-link:hover,
|
||
|
|
.sidebar .nav-link.active {
|
||
|
|
color: #fff;
|
||
|
|
background: rgba(255,255,255,.1);
|
||
|
|
}
|
||
|
|
.sidebar .nav-link.active { background: rgba(99,102,241,.25); color: #a5b4fc; }
|
||
|
|
|
||
|
|
.main-content {
|
||
|
|
margin-left: var(--sidebar-width);
|
||
|
|
min-height: 100vh;
|
||
|
|
padding: 2rem;
|
||
|
|
}
|
||
|
|
.page-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
}
|
||
|
|
.page-header h4 { font-weight: 700; margin: 0; }
|
||
|
|
|
||
|
|
.stat-card { border: none; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||
|
|
.stat-icon {
|
||
|
|
width: 44px; height: 44px;
|
||
|
|
border-radius: 10px;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
font-size: 1.25rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.badge-running { background: #f59e0b; color: #fff; }
|
||
|
|
.badge-success { background: #10b981; color: #fff; }
|
||
|
|
.badge-failed { background: #ef4444; color: #fff; }
|
||
|
|
|
||
|
|
.log-terminal {
|
||
|
|
background: #1e1e2e;
|
||
|
|
color: #cdd6f4;
|
||
|
|
font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace;
|
||
|
|
font-size: .78rem;
|
||
|
|
line-height: 1.5;
|
||
|
|
padding: 1rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
height: 420px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
.log-terminal .log-warn { color: #f9e2af; }
|
||
|
|
.log-terminal .log-error { color: #f38ba8; }
|
||
|
|
.log-terminal .log-ok { color: #a6e3a1; }
|
||
|
|
.log-terminal .log-info { color: #89dceb; }
|
||
|
|
|
||
|
|
.table thead th { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: #64748b; font-weight: 600; }
|
||
|
|
.table td { vertical-align: middle; font-size: .875rem; }
|
||
|
|
|
||
|
|
#backupBtn { min-width: 160px; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
|
||
|
|
<nav class="sidebar">
|
||
|
|
<a href="{{ url_for('dashboard') }}" class="sidebar-brand">
|
||
|
|
<div class="brand-icon"><i class="bi bi-cloud-arrow-up-fill text-white"></i></div>
|
||
|
|
BackupBC
|
||
|
|
</a>
|
||
|
|
<ul class="nav flex-column gap-1">
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link {% if request.endpoint == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">
|
||
|
|
<i class="bi bi-speedometer2"></i> Dashboard
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link {% if request.endpoint == 'history' %}active{% endif %}" href="{{ url_for('history') }}">
|
||
|
|
<i class="bi bi-clock-history"></i> History
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link {% if request.endpoint in ('restore_list','restore_view') %}active{% endif %}" href="{{ url_for('restore_list') }}">
|
||
|
|
<i class="bi bi-arrow-counterclockwise"></i> Restore
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link {% if request.endpoint == 'settings' %}active{% endif %}" href="{{ url_for('settings') }}">
|
||
|
|
<i class="bi bi-gear"></i> Settings
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
<div class="mt-auto pt-3" style="border-top:1px solid rgba(255,255,255,.08); font-size:.7rem; color:rgba(255,255,255,.3)">
|
||
|
|
BackupBC v1.0 · Business Central
|
||
|
|
</div>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<main class="main-content">
|
||
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
|
|
{% if messages %}
|
||
|
|
{% for category, message in messages %}
|
||
|
|
<div class="alert alert-{{ category }} alert-dismissible fade show shadow-sm" role="alert">
|
||
|
|
<i class="bi bi-{% if category == 'success' %}check-circle{% else %}exclamation-triangle{% endif %} me-2"></i>
|
||
|
|
{{ message }}
|
||
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
|
|
</div>
|
||
|
|
{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
{% endwith %}
|
||
|
|
|
||
|
|
{% block content %}{% endblock %}
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<!-- Backup Modal -->
|
||
|
|
<div class="modal fade" id="backupModal" tabindex="-1" data-bs-backdrop="static">
|
||
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||
|
|
<div class="modal-content border-0 shadow-lg">
|
||
|
|
<div class="modal-header border-0 pb-0">
|
||
|
|
<h5 class="modal-title fw-bold">
|
||
|
|
<i class="bi bi-cloud-upload me-2 text-primary"></i>
|
||
|
|
<span id="modalTitle">Starting Backup…</span>
|
||
|
|
</h5>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<div id="modalStatus" class="d-flex align-items-center gap-2 mb-3">
|
||
|
|
<div class="spinner-border spinner-border-sm text-primary" id="modalSpinner" role="status"></div>
|
||
|
|
<span id="modalStatusText" class="text-muted small">Authenticating with Azure AD…</span>
|
||
|
|
</div>
|
||
|
|
<div class="log-terminal" id="logTerminal"></div>
|
||
|
|
</div>
|
||
|
|
<div class="modal-footer border-0 pt-0">
|
||
|
|
<button type="button" class="btn btn-secondary btn-sm" id="modalCloseBtn" data-bs-dismiss="modal" disabled>
|
||
|
|
Close
|
||
|
|
</button>
|
||
|
|
<a href="{{ url_for('history') }}" class="btn btn-primary btn-sm d-none" id="viewHistoryBtn">
|
||
|
|
<i class="bi bi-clock-history me-1"></i>View History
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
|
<script>
|
||
|
|
let _backupModal = null;
|
||
|
|
|
||
|
|
function runBackupNow(forceFull) {
|
||
|
|
if (forceFull === undefined) forceFull = true;
|
||
|
|
|
||
|
|
const terminal = document.getElementById('logTerminal');
|
||
|
|
const statusText = document.getElementById('modalStatusText');
|
||
|
|
const spinner = document.getElementById('modalSpinner');
|
||
|
|
const closeBtn = document.getElementById('modalCloseBtn');
|
||
|
|
const histBtn = document.getElementById('viewHistoryBtn');
|
||
|
|
const title = document.getElementById('modalTitle');
|
||
|
|
|
||
|
|
terminal.innerHTML = '';
|
||
|
|
spinner.classList.remove('d-none');
|
||
|
|
spinner.className = 'spinner-border spinner-border-sm text-primary';
|
||
|
|
statusText.textContent = 'Authenticating with Azure AD…';
|
||
|
|
statusText.className = 'text-muted small';
|
||
|
|
closeBtn.disabled = true;
|
||
|
|
histBtn.classList.add('d-none');
|
||
|
|
title.textContent = forceFull ? 'Full Backup in Progress' : 'Incremental Backup in Progress';
|
||
|
|
|
||
|
|
if (!_backupModal) _backupModal = new bootstrap.Modal(document.getElementById('backupModal'));
|
||
|
|
_backupModal.show();
|
||
|
|
|
||
|
|
fetch('/api/run', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {'Content-Type': 'application/json'},
|
||
|
|
body: JSON.stringify({force_full: forceFull}),
|
||
|
|
})
|
||
|
|
.then(r => r.json())
|
||
|
|
.then(data => {
|
||
|
|
if (data.error) {
|
||
|
|
statusText.textContent = 'Error: ' + data.error;
|
||
|
|
statusText.className = 'text-danger small fw-semibold';
|
||
|
|
spinner.className = 'd-none';
|
||
|
|
closeBtn.disabled = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const runId = data.run_id;
|
||
|
|
statusText.textContent = 'Backup running (run #' + runId + ')…';
|
||
|
|
|
||
|
|
const evtSrc = new EventSource('/api/run/' + runId + '/stream');
|
||
|
|
evtSrc.onmessage = function(e) {
|
||
|
|
const msg = JSON.parse(e.data);
|
||
|
|
if (msg.keepalive) return;
|
||
|
|
if (msg.done) {
|
||
|
|
evtSrc.close();
|
||
|
|
spinner.className = 'd-none';
|
||
|
|
closeBtn.disabled = false;
|
||
|
|
histBtn.classList.remove('d-none');
|
||
|
|
if (msg.status === 'success') {
|
||
|
|
title.textContent = forceFull ? 'Full Backup Complete' : 'Incremental Backup Complete';
|
||
|
|
statusText.innerHTML = '<i class="bi bi-check-circle-fill text-success me-1"></i><span class="text-success fw-semibold">Backup completed successfully!</span>';
|
||
|
|
} else {
|
||
|
|
title.textContent = 'Backup Failed';
|
||
|
|
statusText.innerHTML = '<i class="bi bi-x-circle-fill text-danger me-1"></i><span class="text-danger fw-semibold">Backup failed — see log below.</span>';
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (msg.log) appendLog(terminal, msg.log);
|
||
|
|
};
|
||
|
|
evtSrc.onerror = function() {
|
||
|
|
evtSrc.close();
|
||
|
|
spinner.className = 'd-none';
|
||
|
|
closeBtn.disabled = false;
|
||
|
|
appendLog(terminal, 'Connection lost — check History for result.');
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.catch(err => {
|
||
|
|
statusText.textContent = 'Request failed: ' + err.message;
|
||
|
|
statusText.className = 'text-danger small';
|
||
|
|
spinner.className = 'd-none';
|
||
|
|
closeBtn.disabled = false;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function appendLog(container, text) {
|
||
|
|
const line = document.createElement('div');
|
||
|
|
line.textContent = text;
|
||
|
|
if (/CRITICAL|ERROR/.test(text)) line.className = 'log-error';
|
||
|
|
else if (/WARNING/.test(text)) line.className = 'log-warn';
|
||
|
|
else if (/complete|success|OK/i.test(text)) line.className = 'log-ok';
|
||
|
|
else line.className = 'log-info';
|
||
|
|
container.appendChild(line);
|
||
|
|
container.scrollTop = container.scrollHeight;
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% block scripts %}{% endblock %}
|
||
|
|
</body>
|
||
|
|
</html>
|