414 lines
12 KiB
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 %}
|