snapshot: preserve backup UX, remote target setup, and docs updates
This commit is contained in:
114
backend/workflows/templates/workflows/backup_recovery.html
Normal file
114
backend/workflows/templates/workflows/backup_recovery.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends 'workflows/base_shell.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}{% trans "Backup & Recovery" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'workflows/css/admin_tools.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block shell_body %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %}
|
||||
<h1>{% trans "Backup & Recovery" %}</h1>
|
||||
<p class="sub">{% trans "Datenbank- und Media-Backups erstellen und vorhandene Bundles sicher verifizieren." %}</p>
|
||||
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
|
||||
<section class="card">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<h2 style="margin:0;">{% trans "Aktionen" %}</h2>
|
||||
<div class="hint">{% trans "Erstellung und Verifikation laufen im App-Kontext. Restore bleibt bewusst CLI-only." %}</div>
|
||||
</div>
|
||||
<form method="post" action="{% url 'create_backup_from_admin' %}" data-confirm="{% trans 'Neues Backup jetzt erstellen?' %}" data-progress-title="{% trans 'Backup wird erstellt' %}" data-progress-copy="{% trans 'Bitte warten. Datenbank- und Media-Bundle werden gerade vorbereitet.' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Backup erstellen" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>{% trans "Verfügbare Backup-Bundles" %}</h2>
|
||||
{% if rows %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Bundle" %}</th>
|
||||
<th>{% trans "Erstellt" %}</th>
|
||||
<th>{% trans "Verifiziert" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Inhalt" %}</th>
|
||||
<th>{% trans "Remote" %}</th>
|
||||
<th>{% trans "Aktion" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td><code>{{ row.name }}</code></td>
|
||||
<td>{% if row.created_at %}{{ row.created_at|slice:":16"|cut:"T" }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.verified_at %}{{ row.verified_at|slice:":16"|cut:"T" }}{% else %}-{% endif %}</td>
|
||||
<td>
|
||||
{% if row.verify_status == 'verified' %}
|
||||
<span class="badge sent">{% trans "Verifiziert" %}</span>
|
||||
{% else %}
|
||||
<span class="badge paused">{% trans "Nicht geprüft" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if row.db_dump_exists %}<span class="badge scheduled">DB</span>{% endif %}
|
||||
{% if row.media_archive_exists %}<span class="badge scheduled">Media</span>{% endif %}
|
||||
{% if row.summary %}
|
||||
<div class="hint">{{ row.summary }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if row.remote_status == 'uploaded' %}
|
||||
<span class="badge sent">{% trans "Hochgeladen" %}</span>
|
||||
{% elif row.remote_status == 'failed' %}
|
||||
<span class="badge failed">{% trans "Fehlgeschlagen" %}</span>
|
||||
{% elif row.remote_status == 'not_implemented' %}
|
||||
<span class="badge paused">{% trans "Vorbereitet" %}</span>
|
||||
{% elif row.remote_status == 'disabled' %}
|
||||
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
|
||||
{% else %}
|
||||
<span class="badge paused">{% trans "Lokal" %}</span>
|
||||
{% endif %}
|
||||
{% if row.db_dump_exists or row.media_archive_exists %}
|
||||
<div class="hint">{% trans "Lokal gespeichert" %}</div>
|
||||
{% else %}
|
||||
<div class="hint">{% trans "Lokal nicht vorhanden" %}</div>
|
||||
{% endif %}
|
||||
{% if row.remote_target_type %}
|
||||
<div class="hint">{{ row.remote_target_type|upper }}</div>
|
||||
{% endif %}
|
||||
{% if row.remote_path %}
|
||||
<div class="hint"><code>{{ row.remote_path }}</code></div>
|
||||
{% endif %}
|
||||
{% if row.remote_summary %}
|
||||
<div class="hint">{{ row.remote_summary }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<form method="post" action="{% url 'verify_backup_from_admin' row.name %}" data-confirm="{% trans 'Backup jetzt verifizieren?' %}" data-progress-title="{% trans 'Backup wird verifiziert' %}" data-progress-copy="{% trans 'Bitte warten. Bundle, Datenbank-Dump und Media-Archiv werden geprüft.' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">{% trans "Verifizieren" %}</button>
|
||||
</form>
|
||||
<form method="post" action="{% url 'delete_backup_from_admin' row.name %}" data-confirm="{% trans 'Backup-Bundle wirklich löschen?' %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-secondary" type="submit">{% trans "Löschen" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="hint">{% trans "Noch keine Backup-Bundles vorhanden." %}</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -30,7 +30,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-progress-modal" id="app-action-progress" hidden aria-hidden="true">
|
||||
<div class="action-progress-backdrop"></div>
|
||||
<div class="action-progress-panel" role="status" aria-live="polite" aria-labelledby="app-action-progress-title" aria-describedby="app-action-progress-copy">
|
||||
<p class="action-progress-kicker">{% trans "Bitte warten" %}</p>
|
||||
<h2 class="action-progress-title" id="app-action-progress-title">{% trans "Aktion läuft" %}</h2>
|
||||
<p class="action-progress-copy" id="app-action-progress-copy">{% trans "Die Aktion wird im aktuellen Tab ausgeführt." %}</p>
|
||||
<div class="action-progress-track" aria-hidden="true">
|
||||
<div class="action-progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static 'workflows/js/confirm_dialog.js' %}"></script>
|
||||
<script src="{% static 'workflows/js/action_progress.js' %}"></script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<a href="#nextcloud">Nextcloud</a>
|
||||
<a href="#builders">Builders</a>
|
||||
<a href="#testing">Testing</a>
|
||||
<a href="#backup">Backup</a>
|
||||
<a href="#deploy">Deployment</a>
|
||||
<a href="#troubleshooting">Troubleshooting</a>
|
||||
<a href="#security">Security</a>
|
||||
@@ -152,6 +153,8 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<li>Upload logic lives in <code>backend/workflows/services.py</code>.</li>
|
||||
<li>Feature can be globally toggled without changing environment variables.</li>
|
||||
<li>Failures should degrade gracefully and not block request persistence.</li>
|
||||
<li>Remote backup currently reuses the configured Nextcloud connection, but writes into a separate backup directory configured under <code>Integrationen → Backup-Ziel</code>.</li>
|
||||
<li>Do not point remote backup at the same Nextcloud directory used for normal onboarding/offboarding document uploads.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="builders">10) Builder Architecture</h2>
|
||||
@@ -176,6 +179,7 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<li>Current hooks include builder edits, PDF generation, welcome-email actions, integration changes, mode toggles, tests, and request deletions.</li>
|
||||
<li>Staff UI page: <code>/admin-tools/audit-log/</code></li>
|
||||
<li>The current UI supports filtering by action, user, and date range. Keep filters server-side to avoid loading unbounded audit rows into the browser.</li>
|
||||
<li>Backup UI page: <code>/admin-tools/backups/</code> for create, verify, and delete actions. Keep real restore CLI-only.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="testing">11) Testing and Validation</h2>
|
||||
@@ -193,7 +197,24 @@ docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
|
||||
<li>The Requests Dashboard includes a retry action for failed requests. Retries reset the error text, set the request back to <code>submitted</code>, and enqueue the appropriate Celery task again.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="deploy">12) Deployment and Release Checklist</h2>
|
||||
<h2 id="backup">12) Backup and Restore</h2>
|
||||
<pre><code>make backup-create
|
||||
make backup-verify BACKUP_DIR=backups/backup_YYYYmmdd_HHMMSS</code></pre>
|
||||
<ul>
|
||||
<li>Backups are stored under <code>backend/backups/</code> and ignored by git.</li>
|
||||
<li>Each backup contains a PostgreSQL custom-format dump, a compressed <code>media/</code> archive, metadata, and SHA256 checksums.</li>
|
||||
<li>Backup metadata records both local availability and remote backup status.</li>
|
||||
<li>Remote backup target configuration is managed in <code>Integrationen → Backup-Ziel</code>.</li>
|
||||
<li>Current remote target support: <code>nextcloud</code> implemented, <code>s3</code> and <code>nfs</code> config-ready but not yet implemented.</li>
|
||||
<li>Verification is non-destructive: it restores into a temporary verification database and extracts media into a temporary directory.</li>
|
||||
<li>Real restore is explicit and destructive by design:
|
||||
<pre><code>./scripts/backup_restore.sh --yes-restore backend/backups/backup_YYYYmmdd_HHMMSS</code></pre>
|
||||
</li>
|
||||
<li>Do not run the restore script casually against a live working dataset.</li>
|
||||
<li>The staff UI uses the shared action-progress overlay for backup creation and verification so long-running actions present one standard app behavior.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="deploy">13) Deployment and Release Checklist</h2>
|
||||
<ol>
|
||||
<li>Run <code>manage.py check</code></li>
|
||||
<li>Run tests or targeted verification</li>
|
||||
@@ -206,7 +227,7 @@ docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
|
||||
<li>Take a snapshot commit before major next-phase work</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="troubleshooting">13) Troubleshooting</h2>
|
||||
<h2 id="troubleshooting">14) Troubleshooting</h2>
|
||||
<ul>
|
||||
<li><strong>Page looks stale:</strong> restart <code>web</code> and hard-refresh browser</li>
|
||||
<li><strong>Second request hangs:</strong> inspect web logs and verify health endpoint</li>
|
||||
@@ -217,7 +238,7 @@ docker compose exec -T web python manage.py run_staging_e2e_check</code></pre>
|
||||
<li><strong>Requests dependency warning appears:</strong> verify <code>chardet==5.2.0</code> is installed in the rebuilt image and restart <code>web</code>/<code>worker</code></li>
|
||||
</ul>
|
||||
|
||||
<h2 id="security">14) Security and Maintenance Notes</h2>
|
||||
<h2 id="security">15) Security and Maintenance Notes</h2>
|
||||
<ul>
|
||||
<li>Containers run as non-root <code>app</code> user.</li>
|
||||
<li>Keep secrets in <code>.env</code>, not in tracked files.</li>
|
||||
|
||||
@@ -122,6 +122,11 @@
|
||||
<a class="btn btn-secondary" href="/admin-tools/audit-log/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Backup & Recovery" %}</h3>
|
||||
<p>{% trans "Backups erstellen und sicher verifizieren." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/backups/">{% trans "Öffnen" %}</a>
|
||||
</section>
|
||||
<section class="admin-card">
|
||||
<h3>{% trans "Welcome E-Mails" %}</h3>
|
||||
<p>{% trans "Geplante Welcome Mails verwalten." %}</p>
|
||||
<a class="btn btn-secondary" href="/admin-tools/welcome-emails/">{% trans "Öffnen" %}</a>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block shell_body %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %}
|
||||
{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %}
|
||||
<h1>{% trans "Integrationen Setup" %}</h1>
|
||||
<p class="sub">{% trans "Verwalten Sie Nextcloud- und Mail-Konfiguration ohne Backend-Wechsel." %}</p>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<a class="tab {% if kind == 'mail' %}active{% endif %}" href="/admin-tools/integrations/?kind=mail">{% trans "Setup Mail" %}</a>
|
||||
<a class="tab {% if kind == 'emails' %}active{% endif %}" href="/admin-tools/integrations/?kind=emails">{% trans "E-Mail Routing & Vorlagen" %}</a>
|
||||
<a class="tab {% if kind == 'rules' %}active{% endif %}" href="/admin-tools/integrations/?kind=rules">{% trans "Workflow-Regeln" %}</a>
|
||||
<a class="tab {% if kind == 'backup' %}active{% endif %}" href="/admin-tools/integrations/?kind=backup">{% trans "Backup-Ziel" %}</a>
|
||||
</div>
|
||||
|
||||
{% include 'workflows/includes/messages.html' %}
|
||||
@@ -51,7 +52,7 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Nextcloud speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/nextcloud/">{% trans "Nextcloud-Test starten" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/nextcloud/" data-progress-title="{% trans 'Nextcloud-Test läuft' %}" data-progress-copy="{% trans 'Bitte warten. Verbindung und Upload in das konfigurierte Ziel werden geprüft.' %}">{% trans "Nextcloud-Test starten" %}</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-copy">
|
||||
@@ -113,7 +114,7 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Mail speichern" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/email/">{% trans "SMTP-Test starten" %}</button>
|
||||
<button class="btn btn-secondary" type="submit" formaction="/test/email/" data-progress-title="{% trans 'SMTP-Test läuft' %}" data-progress-copy="{% trans 'Bitte warten. SMTP-Verbindung und Testversand werden geprüft.' %}">{% trans "SMTP-Test starten" %}</button>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-copy">
|
||||
@@ -363,4 +364,67 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if kind == 'backup' %}
|
||||
<form class="card" method="post" action="/admin-tools/integrations/save-backup-settings/" data-remote-backup-form>
|
||||
{% csrf_token %}
|
||||
<div class="grid backup-grid" style="grid-template-columns:1fr">
|
||||
<div class="field-full">
|
||||
<label for="remote_backup_enabled">{% trans "Remote Backup aktiviert" %}</label>
|
||||
<div class="check-row">
|
||||
<label><input id="remote_backup_enabled" type="checkbox" name="remote_backup_enabled" {% if workflow_config.remote_backup_enabled %}checked{% endif %} onchange="window.syncRemoteBackupSettingsVisibility && window.syncRemoteBackupSettingsVisibility(this.form)" /> {% trans "Remote Kopie nach lokalem Bundle erstellen" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" style="grid-template-columns:repeat(2,minmax(240px,1fr)); align-items:end;">
|
||||
<div class="remote-backup-targets">
|
||||
<label for="remote_backup_target_type">{% trans "Remote Backup Zieltyp" %}</label>
|
||||
<select id="remote_backup_target_type" name="remote_backup_target_type" onchange="window.syncRemoteBackupSettingsVisibility && window.syncRemoteBackupSettingsVisibility(this.form)">
|
||||
{% for value, label in remote_backup_target_choices %}
|
||||
<option value="{{ value }}" {% if workflow_config.remote_backup_target_type == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="remote-backup-targets" data-remote-target="nextcloud"{% if not workflow_config.remote_backup_enabled or workflow_config.remote_backup_target_type != 'nextcloud' %} style="display:none"{% endif %}>
|
||||
<label for="remote_backup_nextcloud_directory">{% trans "Nextcloud Backup-Verzeichnis" %}</label>
|
||||
<input id="remote_backup_nextcloud_directory" name="remote_backup_nextcloud_directory" value="{{ workflow_config.remote_backup_nextcloud_directory }}" placeholder="Group-on-off-boarding-backups" />
|
||||
</div>
|
||||
<div class="remote-backup-targets" data-remote-target="s3"{% if not workflow_config.remote_backup_enabled or workflow_config.remote_backup_target_type != 's3' %} style="display:none"{% endif %}>
|
||||
<label for="remote_backup_s3_bucket">{% trans "S3 Bucket (optional)" %}</label>
|
||||
<input id="remote_backup_s3_bucket" name="remote_backup_s3_bucket" value="{{ workflow_config.remote_backup_s3_bucket }}" placeholder="reserved for future support" />
|
||||
</div>
|
||||
<div class="remote-backup-targets" data-remote-target="nfs"{% if not workflow_config.remote_backup_enabled or workflow_config.remote_backup_target_type != 'nfs' %} style="display:none"{% endif %}>
|
||||
<label for="remote_backup_nfs_path">{% trans "NFS Pfad (optional)" %}</label>
|
||||
<input id="remote_backup_nfs_path" name="remote_backup_nfs_path" value="{{ workflow_config.remote_backup_nfs_path }}" placeholder="/mnt/backup-share" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Backup-Einstellungen speichern" %}</button>
|
||||
</div>
|
||||
<div class="hint remote-backup-targets">{% trans "Empfehlung: Nextcloud als erstes Remote-Ziel verwenden. S3 und NFS sind als Zieltypen vorbereitet, aber noch nicht aktiv implementiert." %}</div>
|
||||
<div class="hint remote-backup-targets" data-remote-target="nextcloud"{% if not workflow_config.remote_backup_enabled or workflow_config.remote_backup_target_type != 'nextcloud' %} style="display:none"{% endif %}>{% trans "Das Backup-Verzeichnis muss getrennt vom normalen Nextcloud Dokumentenordner sein, z. B. Group-on-off-boarding-backups." %}</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
{{ block.super }}
|
||||
{% if kind == 'backup' %}
|
||||
<script>
|
||||
window.syncRemoteBackupSettingsVisibility = function (form) {
|
||||
if (!form) return;
|
||||
const target = form.querySelector('#remote_backup_target_type');
|
||||
if (!target) return;
|
||||
const selectedTarget = String(target.value || '').toLowerCase();
|
||||
|
||||
form.querySelectorAll('[data-remote-target]').forEach((node) => {
|
||||
node.style.display = node.dataset.remoteTarget === selectedTarget ? '' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-remote-backup-form]').forEach((form) => {
|
||||
window.syncRemoteBackupSettingsVisibility(form);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
<ul>
|
||||
<li><strong>Form Builder:</strong> manage field visibility/order/options.</li>
|
||||
<li><strong>Einweisungs-Builder:</strong> manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.</li>
|
||||
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules.</li>
|
||||
<li><strong>Integrations:</strong> Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.</li>
|
||||
<li><strong>Welcome Emails:</strong> scheduled jobs, pause/resume/cancel/trigger now.</li>
|
||||
<li><strong>Audit Log:</strong> staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.</li>
|
||||
<li><strong>Requests Dashboard:</strong> search records, open PDFs, delete records (single/bulk for staff).</li>
|
||||
@@ -197,6 +197,12 @@
|
||||
<ul>
|
||||
<li>Container path: <code>/app/media/pdfs/</code></li>
|
||||
<li>Host path: project <code>backend/media/pdfs/</code> via mounted volume.</li>
|
||||
<li>Backup standard: create DB+media bundles under <code>backups/</code> and verify them with a temporary restore before using a real restore.</li>
|
||||
<li>Staff UI shortcut: Admin Apps → <code>Backup & Recovery</code> for create, verify, and delete actions.</li>
|
||||
<li>Each backup row shows both local bundle availability and remote backup state.</li>
|
||||
<li>Remote backup target configuration lives under Admin Apps → <code>Integrationen</code> → <code>Backup-Ziel</code>.</li>
|
||||
<li>Nextcloud remote backups must use a separate backup directory, not the normal onboarding/offboarding document directory.</li>
|
||||
<li>Longer-running admin actions such as backup create/verify and integration tests use the same shared progress overlay after confirmation.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Deployment Notes</h3>
|
||||
|
||||
Reference in New Issue
Block a user