Python backup module (Azure AD OAuth2 → BC API v2.0 → GPG AES256 → S3), Flask web UI on port 1890 with SSE live log streaming, dual incremental + full backup schedules, chain restore merging full + incrementals, and per-entity file browser with ZIP download. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
6.1 KiB
HTML
164 lines
6.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %} — Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h4 class="mb-0"><i class="bi bi-speedometer2 me-2 text-primary"></i>Dashboard</h4>
|
|
{% if running_id %}
|
|
<div class="text-warning small mt-1">
|
|
<i class="bi bi-arrow-repeat me-1"></i>Backup #{{ running_id }} in progress…
|
|
<a href="{{ url_for('history') }}" class="ms-1 link-warning">View log</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-primary" onclick="runBackupNow(true)" id="backupBtn">
|
|
<i class="bi bi-cloud-upload-fill me-1"></i>Full Backup
|
|
</button>
|
|
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
|
data-bs-toggle="dropdown" aria-expanded="false"></button>
|
|
<ul class="dropdown-menu">
|
|
<li>
|
|
<a class="dropdown-item" href="#" onclick="runBackupNow(false); return false;">
|
|
<i class="bi bi-arrow-repeat me-2 text-success"></i>Incremental Backup
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stat cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card stat-card h-100">
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<div class="stat-icon bg-primary bg-opacity-10 text-primary">
|
|
<i class="bi bi-archive-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted small">Total Backups</div>
|
|
<div class="fw-bold fs-4 lh-1">{{ total }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card stat-card h-100">
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<div class="stat-icon bg-success bg-opacity-10 text-success">
|
|
<i class="bi bi-check-circle-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted small">Last Success</div>
|
|
<div class="fw-semibold lh-1 mt-1">{{ last_ok['started_at'] | fmtdt if last_ok else '—' }}</div>
|
|
{% if last_ok %}
|
|
<div class="text-muted" style="font-size:.7rem">{{ last_ok['total_records'] or 0 | int }} records</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card stat-card h-100">
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<div class="stat-icon bg-primary bg-opacity-10 text-primary">
|
|
<i class="bi bi-database-fill-up"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted small">Last Full Backup</div>
|
|
<div class="fw-semibold lh-1 mt-1">{{ last_full['started_at'] | fmtdt if last_full else '—' }}</div>
|
|
{% if last_full %}
|
|
<div class="text-muted" style="font-size:.7rem">{{ '%.1f' | format(last_full['archive_size_mb'] or 0) }} MB</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-6 col-xl-3">
|
|
<div class="card stat-card h-100">
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<div class="stat-icon bg-warning bg-opacity-10 text-warning">
|
|
<i class="bi bi-alarm-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-muted small">Next Incremental</div>
|
|
<div class="fw-semibold lh-1 mt-1" style="font-size:.85rem">{{ next_incr or 'Not scheduled' }}</div>
|
|
<div class="text-muted" style="font-size:.7rem">Full: {{ next_full_run or 'Not scheduled' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent runs -->
|
|
<div class="card stat-card">
|
|
<div class="card-header bg-white border-0 pt-3 pb-2 d-flex justify-content-between align-items-center">
|
|
<span class="fw-bold"><i class="bi bi-clock-history me-2 text-secondary"></i>Recent Backups</span>
|
|
<a href="{{ url_for('history') }}" class="btn btn-outline-secondary btn-sm">View All</a>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Started</th>
|
|
<th>Type</th>
|
|
<th>Trigger</th>
|
|
<th>Companies</th>
|
|
<th>Records</th>
|
|
<th>Size</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in recent %}
|
|
<tr {% if r['status'] == 'failed' %}class="table-danger bg-opacity-25"{% endif %}>
|
|
<td class="text-muted">{{ r['id'] }}</td>
|
|
<td>{{ r['started_at'] | fmtdt }}</td>
|
|
<td>
|
|
{% if r['backup_type'] == 'full' %}
|
|
<span class="badge bg-primary"><i class="bi bi-database-fill-up me-1"></i>Full</span>
|
|
{% else %}
|
|
<span class="badge bg-success"><i class="bi bi-arrow-repeat me-1"></i>Incr</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge rounded-pill {% if r['triggered_by'] == 'manual' %}bg-info{% else %}bg-secondary{% endif %} text-white">
|
|
{{ r['triggered_by'] }}
|
|
</span>
|
|
</td>
|
|
<td class="text-muted small">
|
|
{% if r['companies'] %}
|
|
{{ r['companies'] | replace('["','') | replace('"]','') | replace('","',' · ') }}
|
|
{% else %}—{% endif %}
|
|
</td>
|
|
<td class="text-muted">{{ r['total_records'] or '—' }}</td>
|
|
<td>{% if r['archive_size_mb'] %}{{ '%.1f' | format(r['archive_size_mb']) }} MB{% else %}—{% endif %}</td>
|
|
<td>
|
|
{% if r['status'] == 'success' %}
|
|
<span class="badge badge-success"><i class="bi bi-check me-1"></i>OK</span>
|
|
{% elif r['status'] == 'failed' %}
|
|
<span class="badge badge-failed"><i class="bi bi-x me-1"></i>Failed</span>
|
|
{% else %}
|
|
<span class="badge badge-running"><i class="bi bi-arrow-repeat me-1"></i>Running</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="8" class="text-center text-muted py-5">
|
|
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
|
No backups yet — click <strong>Full Backup</strong> to start.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|