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>
132 lines
5.3 KiB
HTML
132 lines
5.3 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %} — History{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<h4><i class="bi bi-clock-history me-2 text-primary"></i>Backup History</h4>
|
||
<div class="d-flex align-items-center gap-3">
|
||
<span class="text-muted small">{{ total }} total backup{{ 's' if total != 1 }}</span>
|
||
<button class="btn btn-primary btn-sm px-3" onclick="runBackupNow()">
|
||
<i class="bi bi-cloud-upload-fill me-1"></i>Backup Now
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card stat-card">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Started</th>
|
||
<th>Completed</th>
|
||
<th>Duration</th>
|
||
<th>Type</th>
|
||
<th>Trigger</th>
|
||
<th>Companies</th>
|
||
<th>Entities OK</th>
|
||
<th>Records</th>
|
||
<th>Size</th>
|
||
<th>Status</th>
|
||
<th>Archive</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in runs %}
|
||
<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>{{ r['completed_at'] | fmtdt }}</td>
|
||
<td>{{ r['started_at'] | duration(r['completed_at']) }}</td>
|
||
<td>
|
||
{% if r['backup_type'] == 'full' %}
|
||
<span class="badge bg-primary text-white"><i class="bi bi-database-fill-up me-1"></i>Full</span>
|
||
{% else %}
|
||
<span class="badge bg-success text-white"><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-success fw-semibold">{{ r['entities_ok'] or '—' }}</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>Success</span>
|
||
{% elif r['status'] == 'failed' %}
|
||
<span class="badge badge-failed" title="{{ r['error_message'] or '' }}">
|
||
<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>
|
||
<td class="text-muted" style="font-size:.7rem; max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">
|
||
{% if r['s3_key'] %}
|
||
<span title="{{ r['s3_key'] }}">{{ r['s3_key'].split('/')[-1] }}</span>
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if r['s3_key'] and r['status'] == 'success' %}
|
||
<a href="{{ url_for('restore_list') }}"
|
||
class="btn btn-outline-warning btn-sm py-0"
|
||
onclick="event.preventDefault(); sessionStorage.setItem('restoreKey','{{ r['s3_key'] }}'); window.location='{{ url_for('restore_list') }}'">
|
||
<i class="bi bi-arrow-counterclockwise me-1"></i>Restore
|
||
</a>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% if r['status'] == 'failed' and r['error_message'] %}
|
||
<tr class="table-danger">
|
||
<td colspan="13" class="text-danger small py-1 ps-4">
|
||
<i class="bi bi-exclamation-triangle me-1"></i>{{ r['error_message'] }}
|
||
</td>
|
||
</tr>
|
||
{% endif %}
|
||
{% else %}
|
||
<tr>
|
||
<td colspan="13" class="text-center text-muted py-5">
|
||
<i class="bi bi-inbox fs-2 d-block mb-2"></i>No backup history yet
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if pages > 1 %}
|
||
<div class="card-footer bg-white border-0 d-flex justify-content-between align-items-center">
|
||
<span class="text-muted small">Page {{ page }} of {{ pages }}</span>
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0">
|
||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||
<a class="page-link" href="?page={{ page - 1 }}">‹ Prev</a>
|
||
</li>
|
||
{% for p in range(1, pages + 1) %}
|
||
{% if p == page or p == 1 or p == pages or (p >= page - 2 and p <= page + 2) %}
|
||
<li class="page-item {% if p == page %}active{% endif %}">
|
||
<a class="page-link" href="?page={{ p }}">{{ p }}</a>
|
||
</li>
|
||
{% elif p == page - 3 or p == page + 3 %}
|
||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
<li class="page-item {% if page >= pages %}disabled{% endif %}">
|
||
<a class="page-link" href="?page={{ page + 1 }}">Next ›</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|