bmc_hub/app/backups/templates/index.html

821 lines
34 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="da" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backup System - BMC Hub</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root {
--primary-color: #0f4c75;
--secondary-color: #3282b8;
--accent-color: #bbe1fa;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-bg: #f8f9fa;
--dark-bg: #1b2838;
}
[data-bs-theme="dark"] {
--light-bg: #1b2838;
--primary-color: #3282b8;
}
body {
background-color: var(--light-bg);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.navbar {
background-color: var(--primary-color) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card {
border: none;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 1.5rem;
}
.card-header {
background-color: var(--primary-color);
color: white;
border-radius: 10px 10px 0 0 !important;
padding: 1rem 1.25rem;
font-weight: 600;
}
.stat-card {
text-align: center;
padding: 1.5rem;
}
.stat-card .stat-value {
font-size: 2.5rem;
font-weight: bold;
color: var(--primary-color);
}
.stat-card .stat-label {
font-size: 0.9rem;
color: #6c757d;
text-transform: uppercase;
}
.badge-type {
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
.progress {
height: 25px;
font-size: 0.9rem;
}
.btn-action {
margin: 0.25rem;
}
.offsite-badge {
font-size: 0.75rem;
}
.notification-item {
border-left: 4px solid;
padding-left: 1rem;
margin-bottom: 0.75rem;
}
.notification-item.backup_failed {
border-color: var(--danger-color);
}
.notification-item.storage_low {
border-color: var(--warning-color);
}
.notification-item.backup_success {
border-color: var(--success-color);
}
.theme-toggle {
cursor: pointer;
font-size: 1.25rem;
color: white;
}
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="bi bi-hdd-network"></i> BMC Hub - Backup System
</a>
<div class="d-flex align-items-center">
<span class="theme-toggle me-3" onclick="toggleTheme()">
<i class="bi bi-moon-stars" id="theme-icon"></i>
</span>
<a href="/api/docs" class="btn btn-outline-light btn-sm">
<i class="bi bi-code-square"></i> API Docs
</a>
</div>
</div>
</nav>
<div class="container-fluid py-4">
<!-- Stats Row -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-value" id="total-backups">-</div>
<div class="stat-label">Total Backups</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-value text-success" id="completed-backups">-</div>
<div class="stat-label">Completed</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-value text-warning" id="pending-offsite">-</div>
<div class="stat-label">Pending Offsite</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-value" id="storage-usage">-</div>
<div class="stat-label">Storage Used</div>
</div>
</div>
</div>
<!-- Storage Usage -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-hdd"></i> Storage Usage
</div>
<div class="card-body">
<div class="progress" id="storage-progress">
<div class="progress-bar" role="progressbar" style="width: 0%" id="storage-bar">
0%
</div>
</div>
<p class="text-muted mt-2 mb-0" id="storage-details">Loading...</p>
</div>
</div>
</div>
</div>
<!-- Actions and Scheduler Status -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<i class="bi bi-play-circle"></i> Manual Backup
</div>
<div class="card-body">
<form id="backup-form">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label">Backup Type</label>
<select class="form-select" id="backup-type">
<option value="full">Full (Database + Files)</option>
<option value="database">Database Only</option>
<option value="files">Files Only</option>
</select>
</div>
<div class="col-md-4">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="is-monthly">
<label class="form-check-label" for="is-monthly">
Monthly Backup (SQL Format)
</label>
</div>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-download"></i> Create Backup
</button>
</div>
</div>
</form>
<div id="backup-result" class="mt-3"></div>
<hr class="my-4">
<!-- Upload Backup Form -->
<h6 class="mb-3"><i class="bi bi-cloud-upload"></i> Upload Backup</h6>
<form id="upload-form" onsubmit="uploadBackup(event)">
<div class="row g-2">
<div class="col-md-6">
<label for="backup-file" class="form-label">Backup File</label>
<input type="file" class="form-control" id="backup-file" required
accept=".dump,.sql,.sql.gz,.tar.gz,.tgz">
</div>
<div class="col-md-6">
<label for="upload-type" class="form-label">Backup Type</label>
<select class="form-select" id="upload-type" required>
<option value="database">Database</option>
<option value="files">Files</option>
</select>
</div>
<div class="col-md-6">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" id="upload-monthly">
<label class="form-check-label" for="upload-monthly">
Monthly Backup
</label>
</div>
</div>
<div class="col-md-6">
<button type="submit" class="btn btn-success w-100 mt-4">
<i class="bi bi-upload"></i> Upload Backup
</button>
</div>
</div>
</form>
<div id="upload-result" class="mt-3"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<i class="bi bi-clock-history"></i> Scheduler Status
</div>
<div class="card-body">
<div id="scheduler-status">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span class="ms-2">Loading...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Backup History -->
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-clock-history"></i> Backup History</span>
<button class="btn btn-light btn-sm" onclick="refreshBackups()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Format</th>
<th>Size</th>
<th>Status</th>
<th>Offsite</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="backups-table">
<tr>
<td colspan="8" class="text-center">
<div class="spinner-border" role="status"></div>
<p class="mt-2">Loading backups...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Notifications -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<i class="bi bi-bell"></i> Recent Notifications
</div>
<div class="card-body" style="max-height: 500px; overflow-y: auto;">
<div id="notifications-list">
<div class="text-center">
<div class="spinner-border spinner-border-sm" role="status"></div>
<p class="mt-2 text-muted">Loading...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Restore Confirmation Modal -->
<div class="modal fade" id="restoreModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title">
<i class="bi bi-exclamation-triangle"></i> Bekræft Restore
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
<strong>ADVARSEL:</strong> Systemet vil blive lukket ned under restore-processen.
Alle aktive brugere vil miste forbindelsen.
</div>
<p>Er du sikker på, at du vil gendanne fra denne backup?</p>
<p class="text-muted mb-0">
<strong>Backup ID:</strong> <span id="restore-job-id"></span><br>
<strong>Type:</strong> <span id="restore-job-type"></span><br>
<strong>Estimeret tid:</strong> 5-10 minutter
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-danger" onclick="confirmRestore()">
<i class="bi bi-arrow-counterclockwise"></i> Gendan Nu
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let selectedJobId = null;
let restoreModal = null;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
restoreModal = new bootstrap.Modal(document.getElementById('restoreModal'));
loadDashboard();
// Refresh every 30 seconds
setInterval(loadDashboard, 30000);
// Setup backup form
document.getElementById('backup-form').addEventListener('submit', createBackup);
});
// Load all dashboard data
async function loadDashboard() {
await Promise.all([
loadBackups(),
loadStorageStats(),
loadNotifications(),
loadSchedulerStatus()
]);
}
// Load backups list
async function loadBackups() {
// TODO: Implement /api/v1/backups/jobs endpoint
console.warn('⚠️ Backups API ikke implementeret endnu');
document.getElementById('backups-table').innerHTML = '<tr><td colspan="8" class="text-center text-warning"><i class="bi bi-exclamation-triangle me-2"></i>Backup API er ikke implementeret endnu</td></tr>';
return;
/* Disabled until API implemented:
try {
const response = await fetch('/api/v1/backups/jobs?limit=50');
const backups = await response.json();
const tbody = document.getElementById('backups-table');
if (backups.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">No backups found</td></tr>';
updateStats(backups);
return;
}
tbody.innerHTML = backups.map(b => `
<tr>
<td><strong>#${b.id}</strong></td>
<td>
<span class="badge badge-type ${getTypeBadgeClass(b.job_type)}">
${b.job_type} ${b.is_monthly ? '(Monthly)' : ''}
</span>
</td>
<td><code>${b.backup_format}</code></td>
<td>${formatBytes(b.file_size_bytes)}</td>
<td>${getStatusBadge(b.status)}</td>
<td>${getOffsiteBadge(b)}</td>
<td>${formatDate(b.created_at)}</td>
<td>
${b.status === 'completed' ? `
<button class="btn btn-sm btn-warning btn-action" onclick="showRestore(${b.id}, '${b.job_type}')" title="Restore">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
${b.offsite_uploaded_at === null ? `
<button class="btn btn-sm btn-info btn-action" onclick="uploadOffsite(${b.id})" title="Upload Offsite">
<i class="bi bi-cloud-upload"></i>
</button>
` : ''}
` : ''}
<button class="btn btn-sm btn-danger btn-action" onclick="deleteBackup(${b.id})" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`).join('');
updateStats(backups);
} catch (error) {
console.error('Load backups error:', error);
document.getElementById('backups-table').innerHTML =
'<tr><td colspan="8" class="text-center text-danger">Failed to load backups</td></tr>';
}
}
// Load storage stats
async function loadStorageStats() {
// TODO: Implement /api/v1/backups/storage endpoint
return;
/* Disabled until API implemented:
try {
const response = await fetch('/api/v1/backups/storage');
const stats = await response.json();
const bar = document.getElementById('storage-bar');
const pct = Math.min(stats.usage_pct, 100);
bar.style.width = pct + '%';
bar.textContent = pct.toFixed(1) + '%';
if (stats.warning) {
bar.classList.remove('bg-success', 'bg-info');
bar.classList.add('bg-danger');
} else if (pct > 60) {
bar.classList.remove('bg-success', 'bg-danger');
bar.classList.add('bg-warning');
} else {
bar.classList.remove('bg-warning', 'bg-danger');
bar.classList.add('bg-success');
}
document.getElementById('storage-details').textContent =
`${stats.total_size_gb.toFixed(2)} GB used of ${stats.max_size_gb} GB (${stats.file_count} files)`;
document.getElementById('storage-usage').textContent = stats.total_size_gb.toFixed(1) + ' GB';
} catch (error) {
console.error('Load storage stats error:', error);
}
}
// Load notifications
async function loadNotifications() {
// TODO: Implement /api/v1/backups/notifications endpoint
return;
/* Disabled until API implemented:
try {
const response = await fetch('/api/v1/backups/notifications?limit=10');
const notifications = await response.json();
const container = document.getElementById('notifications-list');
if (notifications.length === 0) {
container.innerHTML = '<p class="text-muted text-center">No notifications</p>';
return;
}
container.innerHTML = notifications.map(n => `
<div class="notification-item ${n.event_type} ${n.acknowledged ? 'opacity-50' : ''}">
<small class="text-muted">${formatDate(n.sent_at)}</small>
<p class="mb-1">${n.message}</p>
${!n.acknowledged ? `
<button class="btn btn-sm btn-outline-secondary" onclick="acknowledgeNotification(${n.id})">
<i class="bi bi-check"></i> Acknowledge
</button>
` : '<small class="text-success"><i class="bi bi-check-circle"></i> Acknowledged</small>'}
</div>
`).join('');
} catch (error) {
console.error('Load notifications error:', error);
}
}
// Load scheduler status
async function loadSchedulerStatus() {
// TODO: Implement /api/v1/backups/scheduler/status endpoint
return;
/* Disabled until API implemented:
try {
const response = await fetch('/api/v1/backups/scheduler/status');
const status = await response.json();
const container = document.getElementById('scheduler-status');
if (!status.running) {
container.innerHTML = `
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle"></i> Scheduler not running
</div>
`;
return;
}
container.innerHTML = `
<div class="alert alert-success mb-0">
<i class="bi bi-check-circle"></i> Active
</div>
<small class="text-muted">Next jobs:</small>
<ul class="list-unstyled mb-0 mt-1">
${status.jobs.slice(0, 3).map(j => `
<li><small>${j.name}: ${j.next_run ? formatDate(j.next_run) : 'N/A'}</small></li>
`).join('')}
</ul>
`;
} catch (error) {
console.error('Load scheduler status error:', error);
}
}
// Create manual backup
async function createBackup(event) {
event.preventDefault();
const resultDiv = document.getElementById('backup-result');
resultDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-exclamation-triangle me-2"></i>Backup API er ikke implementeret endnu</div>';
return;
/* Disabled until API implemented:
const type = document.getElementById('backup-type').value;
const isMonthly = document.getElementById('is-monthly').checked;
resultDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> Creating backup...</div>';
try {
const response = await fetch('/api/v1/backups', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({job_type: type, is_monthly: isMonthly})
});
const result = await response.json();
if (response.ok) {
resultDiv.innerHTML = `<div class="alert alert-success">${result.message}</div>`;
setTimeout(() => loadBackups(), 2000);
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${result.detail}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
}
}
// Upload backup
async function uploadBackup(event) {
event.preventDefault();
const resultDiv = document.getElementById('upload-result');
resultDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-exclamation-triangle me-2"></i>Backup upload API er ikke implementeret endnu</div>';
return;
/* Disabled until API implemented:
const fileInput = document.getElementById('backup-file');
const type = document.getElementById('upload-type').value;
const isMonthly = document.getElementById('upload-monthly').checked;
if (!fileInput.files || fileInput.files.length === 0) {
resultDiv.innerHTML = '<div class="alert alert-danger">Please select a file</div>';
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
resultDiv.innerHTML = `<div class="alert alert-info">
<i class="bi bi-hourglass-split"></i> Uploading ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)...
</div>`;
try {
const response = await fetch(`/api/v1/backups/upload?backup_type=${type}&is_monthly=${isMonthly}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
resultDiv.innerHTML = `
<div class="alert alert-success">
<strong>✅ Upload successful!</strong><br>
Job ID: ${result.job_id}<br>
Size: ${result.file_size_mb} MB<br>
Checksum: ${result.checksum.substring(0, 16)}...
</div>
`;
fileInput.value = ''; // Clear file input
setTimeout(() => loadBackups(), 2000);
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${result.detail}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">Upload error: ${error.message}</div>`;
}
}
// Show restore modal
function showRestore(jobId, jobType) {
selectedJobId = jobId;
document.getElementById('restore-job-id').textContent = jobId;
document.getElementById('restore-job-type').textContent = jobType;
restoreModal.show();
}
// Confirm restore
async function confirmRestore() {
alert('⚠️ Restore API er ikke implementeret endnu');
return;
/* Disabled until API implemented:
if (!selectedJobId) return;
try {
const response = await fetch(`/api/v1/backups/restore/${selectedJobId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({confirmation: true})
});
const result = await response.json();
restoreModal.hide();
if (response.ok) {
alert('Restore started! System entering maintenance mode.');
window.location.reload();
} else {
alert('Restore failed: ' + result.detail);
}
} catch (error) {
alert('Restore error: ' + error.message);
}
}
// Upload to offsite
async function uploadOffsite(jobId) {
alert('⚠️ Offsite upload API er ikke implementeret endnu');
return;
/* Disabled until API implemented:
if (!confirm('Upload this backup to offsite storage?')) return;
try {
const response = await fetch(`/api/v1/backups/offsite/${jobId}`, {method: 'POST'});
const result = await response.json();
if (response.ok) {
alert(result.message);
loadBackups();
} else {
alert('Upload failed: ' + result.detail);
}
} catch (error) {
alert('Upload error: ' + error.message);
}
}
// Delete backup
async function deleteBackup(jobId) {
alert('⚠️ Delete backup API er ikke implementeret endnu');
return;
/* Disabled until API implemented:
if (!confirm('Delete this backup? This cannot be undone.')) return;
try {
const response = await fetch(`/api/v1/backups/jobs/${jobId}`, {method: 'DELETE'});
const result = await response.json();
if (response.ok) {
loadBackups();
} else {
alert('Delete failed: ' + result.detail);
}
} catch (error) {
alert('Delete error: ' + error.message);
}
}
// Acknowledge notification
async function acknowledgeNotification(notificationId) {
console.warn('⚠️ Notification API ikke implementeret');
return;
/* Disabled until API implemented:
try {
await fetch(`/api/v1/backups/notifications/${notificationId}/acknowledge`, {method: 'POST'});
loadNotifications();
} catch (error) {
console.error('Acknowledge error:', error);
}
}
// Refresh backups
function refreshBackups() {
loadBackups();
}
// Update stats
function updateStats(backups) {
document.getElementById('total-backups').textContent = backups.length;
document.getElementById('completed-backups').textContent =
backups.filter(b => b.status === 'completed').length;
document.getElementById('pending-offsite').textContent =
backups.filter(b => b.status === 'completed' && !b.offsite_uploaded_at).length;
}
// Utility functions
function formatBytes(bytes) {
if (!bytes) return '-';
const mb = bytes / 1024 / 1024;
return mb > 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb.toFixed(2)} MB`;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('da-DK', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function getTypeBadgeClass(type) {
const classes = {
'database': 'bg-primary',
'files': 'bg-info',
'full': 'bg-success'
};
return classes[type] || 'bg-secondary';
}
function getStatusBadge(status) {
const badges = {
'pending': '<span class="badge bg-secondary">Pending</span>',
'running': '<span class="badge bg-warning">Running</span>',
'completed': '<span class="badge bg-success">Completed</span>',
'failed': '<span class="badge bg-danger">Failed</span>'
};
return badges[status] || status;
}
function getOffsiteBadge(backup) {
if (backup.offsite_uploaded_at) {
return '<span class="badge bg-success offsite-badge"><i class="bi bi-cloud-check"></i> Uploaded</span>';
} else if (backup.offsite_retry_count > 0) {
return `<span class="badge bg-warning offsite-badge"><i class="bi bi-exclamation-triangle"></i> Retry ${backup.offsite_retry_count}</span>`;
} else {
return '<span class="badge bg-secondary offsite-badge">Pending</span>';
}
}
// Theme toggle
function toggleTheme() {
const html = document.documentElement;
const icon = document.getElementById('theme-icon');
const currentTheme = html.getAttribute('data-bs-theme');
if (currentTheme === 'light') {
html.setAttribute('data-bs-theme', 'dark');
icon.classList.remove('bi-moon-stars');
icon.classList.add('bi-sun');
localStorage.setItem('theme', 'dark');
} else {
html.setAttribute('data-bs-theme', 'light');
icon.classList.remove('bi-sun');
icon.classList.add('bi-moon-stars');
localStorage.setItem('theme', 'light');
}
}
// Load saved theme
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-bs-theme', savedTheme);
if (savedTheme === 'dark') {
document.getElementById('theme-icon').classList.replace('bi-moon-stars', 'bi-sun');
}
})();
</script>
</body>
</html>