Spaces:
Paused
Paused
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Legal Dashboard - Reports & Analytics</title> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<!-- Load API Client and Core System --> | |
<script src="js/api-client.js"></script> | |
<script src="js/core.js"></script> | |
<script src="js/notifications.js"></script> | |
<style> | |
:root { | |
--primary-color: #007bff; | |
--success-color: #28a745; | |
--warning-color: #ffc107; | |
--danger-color: #dc3545; | |
--info-color: #17a2b8; | |
} | |
body { | |
background-color: #f8f9fa; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.navbar { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
.navbar-brand { | |
font-weight: bold; | |
color: white ; | |
} | |
.nav-link { | |
color: rgba(255,255,255,0.9) ; | |
transition: color 0.3s ease; | |
} | |
.nav-link:hover { | |
color: white ; | |
} | |
.main-content { | |
padding: 2rem 0; | |
} | |
.card { | |
border: none; | |
border-radius: 15px; | |
box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
margin-bottom: 1.5rem; | |
} | |
.card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 8px 30px rgba(0,0,0,0.15); | |
} | |
.card-header { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
border-radius: 15px 15px 0 0 ; | |
padding: 1.5rem; | |
} | |
.metric-card { | |
text-align: center; | |
padding: 2rem; | |
} | |
.metric-value { | |
font-size: 2.5rem; | |
font-weight: bold; | |
margin-bottom: 0.5rem; | |
} | |
.metric-label { | |
color: #666; | |
font-size: 0.9rem; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
} | |
.metric-change { | |
font-size: 0.8rem; | |
margin-top: 0.5rem; | |
} | |
.metric-change.positive { | |
color: var(--success-color); | |
} | |
.metric-change.negative { | |
color: var(--danger-color); | |
} | |
.chart-container { | |
position: relative; | |
height: 400px; | |
margin: 1rem 0; | |
} | |
.table-responsive { | |
border-radius: 10px; | |
overflow: hidden; | |
} | |
.table { | |
margin-bottom: 0; | |
} | |
.table thead th { | |
background-color: #f8f9fa; | |
border: none; | |
font-weight: 600; | |
color: #495057; | |
} | |
.table tbody tr { | |
transition: background-color 0.2s ease; | |
} | |
.table tbody tr:hover { | |
background-color: #f8f9fa; | |
} | |
.btn-export { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
border: none; | |
color: white; | |
padding: 0.5rem 1.5rem; | |
border-radius: 25px; | |
transition: all 0.3s ease; | |
} | |
.btn-export:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
color: white; | |
} | |
.status-badge { | |
padding: 0.25rem 0.75rem; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
font-weight: 500; | |
} | |
.status-badge.success { | |
background-color: #d4edda; | |
color: #155724; | |
} | |
.status-badge.warning { | |
background-color: #fff3cd; | |
color: #856404; | |
} | |
.status-badge.error { | |
background-color: #f8d7da; | |
color: #721c24; | |
} | |
.loading { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 200px; | |
} | |
.spinner { | |
border: 4px solid #f3f3f3; | |
border-top: 4px solid var(--primary-color); | |
border-radius: 50%; | |
width: 40px; | |
height: 40px; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.filter-section { | |
background: white; | |
border-radius: 15px; | |
padding: 1.5rem; | |
margin-bottom: 2rem; | |
box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
} | |
.date-range { | |
display: flex; | |
gap: 1rem; | |
align-items: center; | |
} | |
.date-input { | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
padding: 0.5rem; | |
font-size: 0.9rem; | |
} | |
.refresh-btn { | |
background: var(--primary-color); | |
border: none; | |
color: white; | |
padding: 0.5rem 1rem; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.refresh-btn:hover { | |
background: #0056b3; | |
transform: translateY(-1px); | |
} | |
.export-section { | |
display: flex; | |
gap: 1rem; | |
flex-wrap: wrap; | |
margin-top: 1rem; | |
} | |
.progress-ring { | |
width: 120px; | |
height: 120px; | |
margin: 0 auto; | |
} | |
.progress-ring circle { | |
fill: none; | |
stroke-width: 8; | |
} | |
.progress-ring .bg { | |
stroke: #e9ecef; | |
} | |
.progress-ring .progress { | |
stroke: var(--primary-color); | |
stroke-linecap: round; | |
transition: stroke-dasharray 0.3s ease; | |
} | |
.progress-text { | |
text-align: center; | |
margin-top: 1rem; | |
} | |
.progress-value { | |
font-size: 1.5rem; | |
font-weight: bold; | |
color: var(--primary-color); | |
} | |
.progress-label { | |
font-size: 0.9rem; | |
color: #666; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- Navigation --> | |
<nav class="navbar navbar-expand-lg navbar-dark"> | |
<div class="container"> | |
<a class="navbar-brand" href="#"> | |
<i class="fas fa-chart-line me-2"></i> | |
Legal Dashboard - Reports | |
</a> | |
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Toggle navigation"> | |
<span class="navbar-toggler-icon"></span> | |
</button> | |
<div class="collapse navbar-collapse" id="navbarNav"> | |
<ul class="navbar-nav ms-auto"> | |
<li class="nav-item"> | |
<a class="nav-link" href="index.html"> | |
<i class="fas fa-home me-1"></i>Dashboard | |
</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="documents.html"> | |
<i class="fas fa-file-alt me-1"></i>Documents | |
</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link active" href="reports.html"> | |
<i class="fas fa-chart-bar me-1"></i>Reports | |
</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="#" onclick="logout()"> | |
<i class="fas fa-sign-out-alt me-1"></i>Logout | |
</a> | |
</li> | |
</ul> | |
</div> | |
</div> | |
</nav> | |
<!-- Main Content --> | |
<div class="container main-content"> | |
<!-- Filter Section --> | |
<div class="filter-section"> | |
<div class="row align-items-center"> | |
<div class="col-md-6"> | |
<h5 class="mb-3"> | |
<i class="fas fa-filter me-2"></i>Filter Options | |
</h5> | |
<div class="date-range"> | |
<label class="me-2">Date Range:</label> | |
<input type="date" id="startDate" class="date-input"> | |
<span>to</span> | |
<input type="date" id="endDate" class="date-input"> | |
<button type="button" class="refresh-btn" onclick="loadReports()"> | |
<i class="fas fa-sync-alt me-1"></i>Refresh | |
</button> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="export-section"> | |
<button type="button" class="btn btn-export" onclick="exportReport('summary')"> | |
<i class="fas fa-download me-1"></i>Export Summary | |
</button> | |
<button type="button" class="btn btn-export" onclick="exportReport('user_activity')"> | |
<i class="fas fa-users me-1"></i>Export User Activity | |
</button> | |
<button type="button" class="btn btn-export" onclick="exportReport('document_analytics')"> | |
<i class="fas fa-file-alt me-1"></i>Export Document Analytics | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Summary Metrics --> | |
<div class="row" id="summaryMetrics"> | |
<div class="col-md-3"> | |
<div class="card metric-card"> | |
<div class="metric-value" id="totalDocuments">-</div> | |
<div class="metric-label">Total Documents</div> | |
<div class="metric-change positive" id="documentsChange">+12% from last month</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card metric-card"> | |
<div class="metric-value" id="totalUsers">-</div> | |
<div class="metric-label">Active Users</div> | |
<div class="metric-change positive" id="usersChange">+5% from last month</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card metric-card"> | |
<div class="metric-value" id="successRate">-</div> | |
<div class="metric-label">Success Rate</div> | |
<div class="metric-change positive" id="successChange">+3% from last month</div> | |
</div> | |
</div> | |
<div class="col-md-3"> | |
<div class="card metric-card"> | |
<div class="metric-value" id="avgProcessingTime">-</div> | |
<div class="metric-label">Avg Processing Time</div> | |
<div class="metric-change negative" id="timeChange">+0.5s from last month</div> | |
</div> | |
</div> | |
</div> | |
<!-- Charts Row --> | |
<div class="row"> | |
<div class="col-md-8"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-chart-line me-2"></i>Document Processing Trends | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="chart-container"> | |
<canvas id="trendsChart"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-4"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-pie-chart me-2"></i>Processing Status | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="chart-container"> | |
<canvas id="statusChart"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Performance Metrics --> | |
<div class="row"> | |
<div class="col-md-6"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-tachometer-alt me-2"></i>System Performance | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="row"> | |
<div class="col-6"> | |
<div class="progress-ring"> | |
<svg width="120" height="120"> | |
<circle class="bg" cx="60" cy="60" r="50"></circle> | |
<circle class="progress" cx="60" cy="60" r="50" | |
stroke-dasharray="314" stroke-dashoffset="78.5"></circle> | |
</svg> | |
</div> | |
<div class="progress-text"> | |
<div class="progress-value">75%</div> | |
<div class="progress-label">CPU Usage</div> | |
</div> | |
</div> | |
<div class="col-6"> | |
<div class="progress-ring"> | |
<svg width="120" height="120"> | |
<circle class="bg" cx="60" cy="60" r="50"></circle> | |
<circle class="progress" cx="60" cy="60" r="50" | |
stroke-dasharray="314" stroke-dashoffset="157"></circle> | |
</svg> | |
</div> | |
<div class="progress-text"> | |
<div class="progress-value">50%</div> | |
<div class="progress-label">Memory Usage</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-6"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-clock me-2"></i>Response Times | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="chart-container"> | |
<canvas id="responseChart"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- User Activity Table --> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-users me-2"></i>User Activity | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="table-responsive"> | |
<table class="table table-hover" id="userActivityTable"> | |
<thead> | |
<tr> | |
<th scope="col">User</th> | |
<th scope="col">Documents Processed</th> | |
<th scope="col">Last Activity</th> | |
<th scope="col">Success Rate</th> | |
<th scope="col">Avg Processing Time</th> | |
</tr> | |
</thead> | |
<tbody id="userActivityBody"> | |
<tr> | |
<td colspan="5" class="text-center"> | |
<div class="loading"> | |
<div class="spinner"></div> | |
</div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
<!-- Document Analytics Table --> | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-file-alt me-2"></i>Recent Document Analytics | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="table-responsive"> | |
<table class="table table-hover" id="documentAnalyticsTable"> | |
<thead> | |
<tr> | |
<th scope="col">Document</th> | |
<th scope="col">Processing Time</th> | |
<th scope="col">OCR Accuracy</th> | |
<th scope="col">File Size</th> | |
<th scope="col">Status</th> | |
<th scope="col">Created</th> | |
</tr> | |
</thead> | |
<tbody id="documentAnalyticsBody"> | |
<tr> | |
<td colspan="6" class="text-center"> | |
<div class="loading"> | |
<div class="spinner"></div> | |
</div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Scripts --> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script src="js/notifications.js"></script> | |
<script> | |
// Global variables | |
let trendsChart, statusChart, responseChart; | |
let currentData = {}; | |
// Initialize page | |
document.addEventListener('DOMContentLoaded', function() { | |
// Set default date range (last 30 days) | |
const endDate = new Date(); | |
const startDate = new Date(); | |
startDate.setDate(startDate.getDate() - 30); | |
document.getElementById('endDate').value = endDate.toISOString().split('T')[0]; | |
document.getElementById('startDate').value = startDate.toISOString().split('T')[0]; | |
// Load initial data | |
loadReports(); | |
}); | |
// Load reports data | |
async function loadReports() { | |
try { | |
showLoading(); | |
// Load summary metrics | |
await loadSummaryMetrics(); | |
// Load charts | |
await loadCharts(); | |
// Load tables | |
await loadUserActivity(); | |
await loadDocumentAnalytics(); | |
hideLoading(); | |
} catch (error) { | |
console.error('Error loading reports:', error); | |
showNotification('error', 'Error', 'Failed to load reports data'); | |
hideLoading(); | |
} | |
} | |
// Load summary metrics | |
async function loadSummaryMetrics() { | |
try { | |
const response = await fetch('/api/reports/summary', { | |
headers: { | |
'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
} | |
}); | |
if (!response.ok) throw new Error('Failed to load summary'); | |
const data = await response.json(); | |
currentData.summary = data; | |
// Update metrics | |
document.getElementById('totalDocuments').textContent = data.total_documents; | |
document.getElementById('totalUsers').textContent = data.total_users; | |
document.getElementById('successRate').textContent = `${data.success_rate.toFixed(1)}%`; | |
document.getElementById('avgProcessingTime').textContent = `${data.avg_processing_time.toFixed(1)}s`; | |
} catch (error) { | |
console.error('Error loading summary metrics:', error); | |
// Set default values | |
document.getElementById('totalDocuments').textContent = '0'; | |
document.getElementById('totalUsers').textContent = '0'; | |
document.getElementById('successRate').textContent = '0%'; | |
document.getElementById('avgProcessingTime').textContent = '0s'; | |
} | |
} | |
// Load charts | |
async function loadCharts() { | |
try { | |
// Load trends data | |
const trendsResponse = await fetch('/api/reports/trends?days=30', { | |
headers: { | |
'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
} | |
}); | |
if (trendsResponse.ok) { | |
const trendsData = await trendsResponse.json(); | |
createTrendsChart(trendsData.daily_trends); | |
} | |
// Create status chart (mock data for now) | |
createStatusChart(); | |
// Create response time chart (mock data for now) | |
createResponseChart(); | |
} catch (error) { | |
console.error('Error loading charts:', error); | |
} | |
} | |
// Create trends chart | |
function createTrendsChart(data) { | |
const ctx = document.getElementById('trendsChart').getContext('2d'); | |
if (trendsChart) { | |
trendsChart.destroy(); | |
} | |
trendsChart = new Chart(ctx, { | |
type: 'line', | |
data: { | |
labels: data.map(d => d.date), | |
datasets: [{ | |
label: 'Documents Processed', | |
data: data.map(d => d.documents_processed), | |
borderColor: '#667eea', | |
backgroundColor: 'rgba(102, 126, 234, 0.1)', | |
tension: 0.4 | |
}, { | |
label: 'Success Rate (%)', | |
data: data.map(d => d.success_rate), | |
borderColor: '#28a745', | |
backgroundColor: 'rgba(40, 167, 69, 0.1)', | |
tension: 0.4, | |
yAxisID: 'y1' | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
y: { | |
beginAtZero: true, | |
title: { | |
display: true, | |
text: 'Documents' | |
} | |
}, | |
y1: { | |
position: 'right', | |
beginAtZero: true, | |
max: 100, | |
title: { | |
display: true, | |
text: 'Success Rate (%)' | |
} | |
} | |
}, | |
plugins: { | |
legend: { | |
position: 'top' | |
} | |
} | |
} | |
}); | |
} | |
// Create status chart | |
function createStatusChart() { | |
const ctx = document.getElementById('statusChart').getContext('2d'); | |
if (statusChart) { | |
statusChart.destroy(); | |
} | |
statusChart = new Chart(ctx, { | |
type: 'doughnut', | |
data: { | |
labels: ['Completed', 'Processing', 'Failed', 'Pending'], | |
datasets: [{ | |
data: [65, 15, 10, 10], | |
backgroundColor: [ | |
'#28a745', | |
'#ffc107', | |
'#dc3545', | |
'#6c757d' | |
] | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
plugins: { | |
legend: { | |
position: 'bottom' | |
} | |
} | |
} | |
}); | |
} | |
// Create response time chart | |
function createResponseChart() { | |
const ctx = document.getElementById('responseChart').getContext('2d'); | |
if (responseChart) { | |
responseChart.destroy(); | |
} | |
responseChart = new Chart(ctx, { | |
type: 'bar', | |
data: { | |
labels: ['Documents', 'OCR', 'Search', 'Analytics'], | |
datasets: [{ | |
label: 'Response Time (ms)', | |
data: [150, 2500, 200, 300], | |
backgroundColor: [ | |
'#667eea', | |
'#764ba2', | |
'#f093fb', | |
'#f5576c' | |
] | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { | |
y: { | |
beginAtZero: true, | |
title: { | |
display: true, | |
text: 'Time (ms)' | |
} | |
} | |
}, | |
plugins: { | |
legend: { | |
display: false | |
} | |
} | |
} | |
}); | |
} | |
// Load user activity | |
async function loadUserActivity() { | |
try { | |
const response = await fetch('/api/reports/user-activity?days=30', { | |
headers: { | |
'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
} | |
}); | |
if (!response.ok) throw new Error('Failed to load user activity'); | |
const data = await response.json(); | |
currentData.userActivity = data; | |
const tbody = document.getElementById('userActivityBody'); | |
tbody.innerHTML = ''; | |
data.forEach(user => { | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td><strong>${user.username}</strong></td> | |
<td>${user.documents_processed}</td> | |
<td>${formatDate(user.last_activity)}</td> | |
<td><span class="status-badge success">${user.success_rate.toFixed(1)}%</span></td> | |
<td>${user.total_processing_time.toFixed(1)}s</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} catch (error) { | |
console.error('Error loading user activity:', error); | |
document.getElementById('userActivityBody').innerHTML = | |
'<tr><td colspan="5" class="text-center text-muted">No data available</td></tr>'; | |
} | |
} | |
// Load document analytics | |
async function loadDocumentAnalytics() { | |
try { | |
const response = await fetch('/api/reports/document-analytics?limit=20', { | |
headers: { | |
'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
} | |
}); | |
if (!response.ok) throw new Error('Failed to load document analytics'); | |
const data = await response.json(); | |
currentData.documentAnalytics = data; | |
const tbody = document.getElementById('documentAnalyticsBody'); | |
tbody.innerHTML = ''; | |
data.forEach(doc => { | |
const row = document.createElement('tr'); | |
row.innerHTML = ` | |
<td><strong>${doc.filename}</strong></td> | |
<td>${doc.processing_time.toFixed(1)}s</td> | |
<td>${doc.ocr_accuracy ? doc.ocr_accuracy.toFixed(1) + '%' : 'N/A'}</td> | |
<td>${formatFileSize(doc.file_size)}</td> | |
<td><span class="status-badge ${getStatusClass(doc.status)}">${doc.status}</span></td> | |
<td>${formatDate(doc.created_at)}</td> | |
`; | |
tbody.appendChild(row); | |
}); | |
} catch (error) { | |
console.error('Error loading document analytics:', error); | |
document.getElementById('documentAnalyticsBody').innerHTML = | |
'<tr><td colspan="6" class="text-center text-muted">No data available</td></tr>'; | |
} | |
} | |
// Export report | |
async function exportReport(type) { | |
try { | |
const response = await fetch(`/api/reports/export/csv?report_type=${type}`, { | |
headers: { | |
'Authorization': `Bearer ${localStorage.getItem('auth_token')}` | |
} | |
}); | |
if (!response.ok) throw new Error('Failed to export report'); | |
const blob = await response.blob(); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = `${type}_report_${new Date().toISOString().split('T')[0]}.csv`; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
window.URL.revokeObjectURL(url); | |
showNotification('success', 'Export Successful', `${type} report has been downloaded`); | |
} catch (error) { | |
console.error('Error exporting report:', error); | |
showNotification('error', 'Export Failed', 'Failed to export report'); | |
} | |
} | |
// Utility functions | |
function formatDate(dateString) { | |
if (!dateString || dateString === 'Never') return 'Never'; | |
const date = new Date(dateString); | |
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); | |
} | |
function formatFileSize(bytes) { | |
if (bytes === 0) return '0 Bytes'; | |
const k = 1024; | |
const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
} | |
function getStatusClass(status) { | |
switch (status.toLowerCase()) { | |
case 'completed': return 'success'; | |
case 'processing': return 'warning'; | |
case 'failed': return 'error'; | |
default: return 'warning'; | |
} | |
} | |
function showLoading() { | |
// Show loading indicators | |
const loadingElements = document.querySelectorAll('.loading'); | |
loadingElements.forEach(el => el.style.display = 'flex'); | |
} | |
function hideLoading() { | |
// Hide loading indicators | |
const loadingElements = document.querySelectorAll('.loading'); | |
loadingElements.forEach(el => el.style.display = 'none'); | |
} | |
function logout() { | |
localStorage.removeItem('auth_token'); | |
window.location.href = 'index.html'; | |
} | |
// Auto-refresh every 5 minutes | |
setInterval(loadReports, 300000); | |
</script> | |
</body> | |
</html> |