mirror of
https://github.com/acedanger/shell.git
synced 2025-12-05 22:50:18 -08:00
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:
216
static/css/custom.css
Normal file
216
static/css/custom.css
Normal 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
159
static/js/app.js
Normal 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
|
||||
Reference in New Issue
Block a user