vSphere-Backup-Manager/templates/reports.html

414 lines
12 KiB
HTML

{% extends "base.html" %}
{% set active_page = 'reports' %}
{% block title %}Reports & Analytics — vSphere Backup Manager{% endblock %}
{% block head %}
<!-- Chart.js from CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
transition: transform 0.2s, border-color 0.2s;
}
.metric-card:hover {
transform: translateY(-2px);
border-color: var(--border-bright);
}
.metric-value {
font-size: 28px;
font-weight: 800;
margin-top: 6px;
letter-spacing: -0.02em;
}
.metric-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 24px;
}
@media (max-width: 900px) {
.charts-grid { grid-template-columns: 1fr; }
}
.chart-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
}
.chart-title {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.history-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
margin-bottom: 32px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
gap: 16px;
flex-wrap: wrap;
}
.history-title {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
}
.search-input-wrap {
position: relative;
max-width: 320px;
width: 100%;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
font-size: 13.5px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
font-size: 14px;
}
.runs-table-wrap {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.runs-table-wrap table {
width: 100%;
border-collapse: collapse;
}
.runs-table-wrap th, .runs-table-wrap td {
padding: 12px 16px;
text-align: left;
}
.runs-table-wrap th {
background: rgba(255, 255, 255, 0.02);
border-bottom: 1px solid var(--border);
font-size: 11.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.runs-table-wrap td {
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.runs-table-wrap tr:last-child td {
border-bottom: none;
}
.runs-table-wrap tr:hover td {
background: rgba(255, 255, 255, 0.01);
}
.text-right { text-align: right !important; }
</style>
{% endblock %}
{% block content %}
<div class="topbar">
<div>
<div class="topbar-title">Reports & Analytics</div>
<div class="topbar-subtitle">System backup telemetry, storage usage growth, and runs logs history</div>
</div>
</div>
<div class="content">
<!-- Summary Metrics Cards -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Total Backups</div>
<div class="metric-value" style="color: var(--text-primary);">{{ stats.total }}</div>
</div>
<div class="metric-card">
<div class="metric-label">Success Rate</div>
<div class="metric-value" style="color: var(--success);">{{ stats.success_rate }}%</div>
</div>
<div class="metric-card">
<div class="metric-label">Total Storage Saved</div>
<div class="metric-value" style="color: var(--accent-2);">{{ stats.total_size_gb }} GB</div>
</div>
<div class="metric-card">
<div class="metric-label">Avg Duration</div>
<div class="metric-value" style="color: var(--accent);">{{ stats.avg_duration_fmt }}</div>
</div>
</div>
<!-- Trends Charts -->
<div class="charts-grid">
<!-- Backup Size Chart -->
<div class="chart-card">
<div class="chart-title">
<span>💾</span> Backup Size Trend (Last 15 Runs)
</div>
<div style="position: relative; height: 260px; width: 100%;">
<canvas id="sizeChart"></canvas>
</div>
</div>
<!-- Backup Duration Chart -->
<div class="chart-card">
<div class="chart-title">
<span>⏱️</span> Backup Duration Trend (Last 15 Runs)
</div>
<div style="position: relative; height: 260px; width: 100%;">
<canvas id="durationChart"></canvas>
</div>
</div>
</div>
<!-- Historical Run Logs Table -->
<div class="history-card">
<div class="history-header">
<div class="history-title">Backup Run History</div>
<div class="search-input-wrap">
<span class="search-icon">🔍</span>
<input type="text" id="runSearch" class="form-control search-input" placeholder="Search by VM, Job or Status..." />
</div>
</div>
<div class="runs-table-wrap">
<table>
<thead>
<tr>
<th>Job Details</th>
<th>Virtual Machine</th>
<th>Size (GB)</th>
<th>Duration</th>
<th>Start Time</th>
<th>Alert Status</th>
<th class="text-right">Status</th>
</tr>
</thead>
<tbody id="runsTableBody">
{% if runs %}
{% for run in runs %}
<tr class="run-row" data-search="{{ run.job_label|lower }} {{ run.job_id|lower }} {{ run.vm_name|lower }} {{ run.status|lower }}">
<td>
<div style="font-weight: 600; color: var(--text-primary);">{{ run.job_label or 'Job (No Label)' }}</div>
<div class="text-small text-muted mono" style="font-size: 11px;">ID: {{ run.job_id[:12] }}...</div>
</td>
<td>
<span style="font-weight: 500; color: var(--text-secondary);">{{ run.vm_name }}</span>
</td>
<td>{{ run.size_gb }} GB</td>
<td>{{ run.duration_fmt }}</td>
<td>
<div style="font-size: 12.5px;">{{ run.started_fmt }}</div>
</td>
<td>
{% if run.notification_sent %}
<span style="color: var(--success); font-weight: 600; display: inline-flex; align-items: center; gap: 4px;">
<span style="font-size: 11px;">🟢</span> Sent
</span>
{% else %}
<span style="color: var(--text-muted); display: inline-flex; align-items: center; gap: 4px;">
<span style="font-size: 11px;"></span> Skipped/None
</span>
{% endif %}
</td>
<td class="text-right">
{% if run.status.lower() in ('finished', 'success') %}
<span class="badge badge-green">Success</span>
{% elif 'failed' in run.status.lower() %}
<span class="badge badge-red" title="{{ run.status }}">Failed</span>
{% elif 'error' in run.status.lower() %}
<span class="badge badge-yellow" title="{{ run.status }}">Error</span>
{% else %}
<span class="badge badge-gray" title="{{ run.status }}">{{ run.status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="7" style="text-align: center; padding: 48px; color: var(--text-muted);">
No historical backup runs logged yet. Runs will appear here as scheduled or manual backups complete.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Dynamic JavaScript Search Filtering
const searchInput = document.getElementById('runSearch');
const tableRows = document.querySelectorAll('.run-row');
if (searchInput) {
searchInput.addEventListener('input', function() {
const query = this.value.toLowerCase().trim();
let matches = 0;
tableRows.forEach(row => {
const searchText = row.getAttribute('data-search');
if (searchText.includes(query)) {
row.style.display = '';
matches++;
} else {
row.style.display = 'none';
}
});
let noMatchRow = document.getElementById('no-runs-match');
if (matches === 0 && tableRows.length > 0) {
if (!noMatchRow) {
const tbody = document.getElementById('runsTableBody');
noMatchRow = document.createElement('tr');
noMatchRow.id = 'no-runs-match';
noMatchRow.innerHTML = '<td colspan="7" style="text-align:center;padding:32px;color:var(--text-muted);">No matching runs found.</td>';
tbody.appendChild(noMatchRow);
}
} else if (noMatchRow) {
noMatchRow.remove();
}
});
}
// Load and render Chart.js plots
document.addEventListener("DOMContentLoaded", function() {
fetch('/api/reports-data')
.then(response => response.json())
.then(data => {
const runs = data.runs || [];
// Extract chart vectors
const labels = runs.map(r => r.date);
const sizes = runs.map(r => r.size_gb);
const durations = runs.map(r => r.duration_sec);
const statuses = runs.map(r => r.status);
// Size Chart Settings
const sizeCtx = document.getElementById('sizeChart').getContext('2d');
const sizeGrad = sizeCtx.createLinearGradient(0, 0, 0, 240);
sizeGrad.addColorStop(0, 'rgba(6, 182, 212, 0.4)');
sizeGrad.addColorStop(1, 'rgba(6, 182, 212, 0.0)');
new Chart(sizeCtx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Backup Size (GB)',
data: sizes,
borderColor: '#06b6d4',
borderWidth: 2.5,
backgroundColor: sizeGrad,
fill: true,
tension: 0.35,
pointBackgroundColor: '#06b6d4',
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#94a3b8', font: { size: 10 } }
},
y: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#94a3b8', font: { size: 10 } }
}
}
}
});
// Duration Chart Settings
const durationCtx = document.getElementById('durationChart').getContext('2d');
const durationGrad = durationCtx.createLinearGradient(0, 0, 0, 240);
durationGrad.addColorStop(0, 'rgba(99, 102, 241, 0.4)');
durationGrad.addColorStop(1, 'rgba(99, 102, 241, 0.0)');
new Chart(durationCtx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Duration (Seconds)',
data: durations,
borderColor: '#6366f1',
borderWidth: 2.5,
backgroundColor: durationGrad,
fill: true,
tension: 0.35,
pointBackgroundColor: '#6366f1',
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#94a3b8', font: { size: 10 } }
},
y: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#94a3b8', font: { size: 10 } }
}
}
}
});
})
.catch(err => console.error("Error drawing charts:", err));
});
</script>
{% endblock %}