Christian Mueller 06253e3fef feat: backup system for portal DB and WLED configs
Portal DB:
- Download current DB button
- Manual backup creation
- Auto-backup daily at 02:00 (keeps last 14)
- Download/delete individual backups

WLED Config:
- Backup all devices at once
- Backup individual device
- Download config as JSON
- Restore config to any device
- Delete old backups

New sidebar menu item 'Backup' with dedicated page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 12:55:06 +02:00

209 lines
11 KiB
HTML

{% extends "base.html" %}
{% block title %}Backup - Busylight{% endblock %}
{% block content %}
<h4 class="mb-4">Backup</h4>
{% if request.query_params.get('db_created') %}
<div class="alert alert-success alert-dismissible fade show">
<i class="bi bi-check-circle"></i> Portal-Backup erstellt.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if request.query_params.get('wled_backed_up') %}
<div class="alert alert-success alert-dismissible fade show">
<i class="bi bi-check-circle"></i> {{ request.query_params.get('wled_backed_up') }} Tuerschild-Config(s) gesichert.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if request.query_params.get('restored') == '1' %}
<div class="alert alert-success alert-dismissible fade show">
<i class="bi bi-check-circle"></i> WLED-Config erfolgreich wiederhergestellt.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% elif request.query_params.get('restored') == '0' %}
<div class="alert alert-danger alert-dismissible fade show">
<i class="bi bi-x-circle"></i> WLED-Config konnte nicht wiederhergestellt werden.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<div class="row g-4">
<!-- Portal DB Backup -->
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-database"></i> Portal-Datenbank</h6>
<div>
<a href="/api/backup/db/download" class="btn btn-sm btn-outline-primary">
<i class="bi bi-download"></i> Aktuelle DB
</a>
<form method="post" action="/api/backup/db/create" class="d-inline">
<button type="submit" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle"></i> Backup erstellen
</button>
</form>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Datei</th>
<th>Groesse</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for b in db_backups %}
<tr>
<td><small class="font-monospace">{{ b.name }}</small></td>
<td>{{ b.size }}</td>
<td>
<a href="/api/backup/db/{{ b.name }}" class="btn btn-sm btn-outline-primary" title="Download">
<i class="bi bi-download"></i>
</a>
<form method="post" action="/api/backup/db/{{ b.name }}/delete" class="d-inline"
onsubmit="return confirm('Backup loeschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Loeschen">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
{% if not db_backups %}
<tr>
<td colspan="3" class="text-center text-muted py-3">Noch keine Backups vorhanden</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="card-footer text-muted">
<small><i class="bi bi-clock"></i> Auto-Backup taeglich um 02:00 Uhr (max. 14 Backups)</small>
</div>
</div>
</div>
<!-- WLED Config Backup -->
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-router"></i> Tuerschild-Konfigurationen</h6>
<div>
<form method="post" action="/api/backup/wled/all" class="d-inline">
<button type="submit" class="btn btn-sm btn-primary">
<i class="bi bi-download"></i> Alle sichern
</button>
</form>
</div>
</div>
<div class="card-body p-0">
<!-- Einzelnes Tuerschild sichern -->
<div class="p-3 border-bottom bg-light">
<form method="post" class="d-flex gap-2 align-items-end" id="single-backup-form">
<div class="flex-grow-1">
<label class="form-label small text-muted mb-1">Einzelnes Tuerschild sichern</label>
<select class="form-select form-select-sm" id="backup-device-select">
{% for d in devices %}
<option value="{{ d.id }}">{{ d.name or d.hostname }} ({{ d.hostname }})</option>
{% endfor %}
</select>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="backupSingle()">
<i class="bi bi-download"></i>
</button>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Datei</th>
<th>Groesse</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for b in wled_backups %}
<tr>
<td><small class="font-monospace">{{ b.name }}</small></td>
<td>{{ b.size }}</td>
<td>
<a href="/api/backup/wled/download/{{ b.name }}" class="btn btn-sm btn-outline-primary" title="Download">
<i class="bi bi-download"></i>
</a>
<button class="btn btn-sm btn-outline-warning" title="Wiederherstellen"
data-bs-toggle="modal" data-bs-target="#restore-{{ loop.index }}">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<form method="post" action="/api/backup/wled/delete/{{ b.name }}" class="d-inline"
onsubmit="return confirm('Backup loeschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Loeschen">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
<!-- Restore Modal -->
<div class="modal fade" id="restore-{{ loop.index }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="/api/backup/wled/restore/{{ b.name }}">
<div class="modal-header">
<h5 class="modal-title">Config wiederherstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Config <strong>{{ b.name }}</strong> auf welches Tuerschild uebertragen?</p>
<select name="device_id" class="form-select">
{% for d in devices %}
<option value="{{ d.id }}">{{ d.name or d.hostname }} ({{ d.hostname }})</option>
{% endfor %}
</select>
<div class="alert alert-warning mt-3 mb-0">
<small><i class="bi bi-exclamation-triangle"></i> Das Tuerschild startet nach dem Restore neu!</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-warning">Wiederherstellen</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
{% if not wled_backups %}
<tr>
<td colspan="3" class="text-center text-muted py-3">Noch keine Config-Backups vorhanden</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function backupSingle() {
const id = document.getElementById('backup-device-select').value;
const form = document.createElement('form');
form.method = 'POST';
form.action = '/api/backup/wled/' + id;
document.body.appendChild(form);
form.submit();
}
</script>
{% endblock %}