feat: Add base HTML template and implement dashboard, logs, and service views

- Created a base HTML template for consistent layout across pages.
- Developed a dashboard page to display backup service metrics and statuses.
- Implemented a log viewer for detailed log file inspection.
- Added error handling page for better user experience during failures.
- Introduced service detail page to show specific service metrics and actions.
- Enhanced log filtering and viewing capabilities.
- Integrated auto-refresh functionality for real-time updates on metrics.
- Created integration and unit test scripts for backup metrics functionality.
This commit is contained in:
Peter Wood
2025-06-18 08:06:08 -04:00
parent d066f32b10
commit 6d726cb015
34 changed files with 6006 additions and 26 deletions

216
static/css/custom.css Normal file
View File

@@ -0,0 +1,216 @@
/* Custom CSS for Backup Monitor */
.service-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.service-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.status-success {
color: #28a745;
}
.status-partial {
color: #ffc107;
}
.status-failed {
color: #dc3545;
}
.status-running {
color: #007bff;
}
.status-unknown {
color: #6c757d;
}
.navbar-brand {
font-weight: bold;
}
.card-header {
border-bottom: 2px solid #f8f9fa;
}
.service-card .card-body {
min-height: 200px;
}
.btn-group-sm > .btn, .btn-sm {
font-size: 0.8rem;
}
/* Loading spinner */
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.display-4 {
font-size: 2rem;
}
.service-card .card-body {
min-height: auto;
}
}
/* Status indicators */
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-indicator.success {
background-color: #28a745;
}
.status-indicator.warning {
background-color: #ffc107;
}
.status-indicator.danger {
background-color: #dc3545;
}
.status-indicator.info {
background-color: #17a2b8;
}
.status-indicator.secondary {
background-color: #6c757d;
}
/* Custom alert styles */
.alert-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Card hover effects */
.card {
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.375rem;
}
.card:hover {
border-color: rgba(0,123,255,.25);
}
/* Footer styling */
footer {
margin-top: auto;
}
/* Utility classes */
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cursor-pointer {
cursor: pointer;
}
/* Animation for refresh button */
.btn .fa-sync-alt {
transition: transform 0.3s ease;
}
.btn:hover .fa-sync-alt {
transform: rotate(180deg);
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.card {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.card-header {
background-color: #4a5568;
border-color: #718096;
}
.text-muted {
color: #a0aec0 !important;
}
}
/* Text contrast and visibility fixes */
.card {
background-color: #ffffff !important;
color: #212529 !important;
}
.card-header {
background-color: #f8f9fa !important;
color: #212529 !important;
}
.card-body {
background-color: #ffffff !important;
color: #212529 !important;
}
.card-footer {
background-color: #f8f9fa !important;
color: #212529 !important;
}
/* Ensure table text is visible */
.table {
color: #212529 !important;
}
.table td, .table th {
color: #212529 !important;
}
/* Service detail page text fixes */
.text-muted {
color: #6c757d !important;
}
/* Alert text visibility */
.alert {
color: #212529 !important;
}
.alert-success {
background-color: #d4edda !important;
border-color: #c3e6cb !important;
color: #155724 !important;
}
.alert-warning {
background-color: #fff3cd !important;
border-color: #ffeaa7 !important;
color: #856404 !important;
}
.alert-danger {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24 !important;
}
.alert-info {
background-color: #d1ecf1 !important;
border-color: #bee5eb !important;
color: #0c5460 !important;
}

159
static/js/app.js Normal file
View File

@@ -0,0 +1,159 @@
// JavaScript for Backup Monitor
document.addEventListener('DOMContentLoaded', function() {
console.log('Backup Monitor loaded');
// Update last updated time
updateLastUpdatedTime();
// Set up auto-refresh
setupAutoRefresh();
// Set up service card interactions
setupServiceCards();
});
function updateLastUpdatedTime() {
const lastUpdatedElement = document.getElementById('last-updated');
if (lastUpdatedElement) {
const now = new Date();
lastUpdatedElement.textContent = `Last updated: ${now.toLocaleTimeString()}`;
}
}
function setupAutoRefresh() {
// Auto-refresh every 30 seconds
setInterval(function() {
console.log('Auto-refreshing metrics...');
refreshMetrics();
}, 30000);
}
function setupServiceCards() {
// Add click handlers for service cards
const serviceCards = document.querySelectorAll('.service-card');
serviceCards.forEach(card => {
card.addEventListener('click', function(e) {
// Don't trigger if clicking on buttons
if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') {
return;
}
const serviceName = this.dataset.service;
if (serviceName) {
window.location.href = `/service/${serviceName}`;
}
});
// Add hover effects
card.style.cursor = 'pointer';
});
}
function refreshMetrics() {
// Show loading indicator
const refreshButton = document.querySelector('[onclick="refreshMetrics()"]');
if (refreshButton) {
const icon = refreshButton.querySelector('i');
if (icon) {
icon.classList.add('fa-spin');
}
refreshButton.disabled = true;
}
// Reload the page to get fresh data
setTimeout(() => {
location.reload();
}, 500);
}
function downloadBackup(serviceName) {
console.log(`Downloading backup for service: ${serviceName}`);
// Create a temporary link to trigger download
const link = document.createElement('a');
link.href = `/api/backup/download/${serviceName}`;
link.download = `${serviceName}-backup.tar.gz`;
link.target = '_blank';
// Append to body, click, and remove
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Utility functions
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDuration(seconds) {
if (seconds < 60) {
return `${seconds}s`;
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
}
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
// Add to page
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
// Health check functionality
function checkSystemHealth() {
fetch('/health')
.then(response => response.json())
.then(data => {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
if (data.status === 'healthy') {
statusIndicator.className = 'text-success';
statusIndicator.innerHTML = '<i class="fas fa-circle me-1"></i>Online';
} else {
statusIndicator.className = 'text-warning';
statusIndicator.innerHTML = '<i class="fas fa-exclamation-circle me-1"></i>Issues';
}
}
})
.catch(error => {
console.error('Health check failed:', error);
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.className = 'text-danger';
statusIndicator.innerHTML = '<i class="fas fa-times-circle me-1"></i>Offline';
}
});
}
// Run health check every minute
setInterval(checkSystemHealth, 60000);
checkSystemHealth(); // Run immediately