Files
BC-bak/templates/dashboard.html
Malin b651eca285 feat: Business Central backup Docker container with web management panel
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>
2026-06-26 20:09:32 +02:00

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 %}