7e7ec67e58
- fix: imap folder names with spaces (RFC3501 quoting in move/create/select) - fix: move only deletes source after COPY + Message-ID verification in target - fix: backup endpoint hung on sqlite write locks — enable WAL + busy_timeout - perf: indexes on filter_logs(created_at, level, account_id+created_at) for fast log queries on millions of rows - feat: optional "logs mit sichern" checkbox in backup export, restore on import - UI: backup download uses fetch+blob with error display instead of location.href Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
6.0 KiB
HTML
149 lines
6.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Import / Export — IMAP Mail Filter{% endblock %}
|
|
{% block content %}
|
|
<h1>Import / Export</h1>
|
|
|
|
<h2>Komplett-Backup</h2>
|
|
<p>Enthält alles: Konten (inkl. Passwörter), Filterregeln und Verarbeitungsstatus. Der Encryption-Key muss beim Import identisch sein.</p>
|
|
<div class="grid">
|
|
<article>
|
|
<header><h3>Backup erstellen</h3></header>
|
|
<p>Gesamte Konfiguration als JSON-Datei herunterladen.</p>
|
|
<label style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.5rem;">
|
|
<input type="checkbox" id="backup-include-logs" style="margin-bottom:0;">
|
|
Logs mit sichern <small style="opacity:0.7;">(Datei wird deutlich größer)</small>
|
|
</label>
|
|
<button id="backup-download-btn" onclick="exportBackup()">Backup herunterladen</button>
|
|
<div id="backup-export-status" style="margin-top:0.5rem;"></div>
|
|
</article>
|
|
|
|
<article>
|
|
<header><h3>Backup wiederherstellen</h3></header>
|
|
<p>Backup-Datei hochladen. Bestehende Konten werden aktualisiert, neue angelegt.</p>
|
|
<form id="backup-import-form">
|
|
<input type="file" name="file" accept=".json" required>
|
|
<button type="submit">Wiederherstellen</button>
|
|
</form>
|
|
<div id="backup-result" style="display:none"></div>
|
|
</article>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h2>YAML Filterregeln</h2>
|
|
<p>Nur Konten und Filterregeln — ohne Passwörter (können als Umgebungsvariablen referenziert werden).</p>
|
|
<div class="grid">
|
|
<article>
|
|
<header><h3>YAML Export</h3></header>
|
|
<button onclick="exportYaml()">YAML exportieren</button>
|
|
<pre id="yaml-preview" style="max-height: 300px; overflow-y: auto; display: none; margin-top: 1rem;"></pre>
|
|
</article>
|
|
|
|
<article>
|
|
<header><h3>YAML Import</h3></header>
|
|
<form id="yaml-import-form">
|
|
<input type="file" name="file" accept=".yaml,.yml" required>
|
|
<button type="submit">Importieren</button>
|
|
</form>
|
|
<div id="yaml-result" style="display:none"></div>
|
|
</article>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
async function exportBackup() {
|
|
const includeLogs = document.getElementById('backup-include-logs').checked;
|
|
const btn = document.getElementById('backup-download-btn');
|
|
const statusDiv = document.getElementById('backup-export-status');
|
|
btn.setAttribute('aria-busy', 'true');
|
|
btn.disabled = true;
|
|
statusDiv.innerHTML = `<small>Backup wird erstellt${includeLogs ? ' (Logs werden geladen, kann dauern...)' : ''}</small>`;
|
|
try {
|
|
const url = `/api/yaml/backup${includeLogs ? '?include_logs=true' : ''}`;
|
|
const resp = await fetch(url);
|
|
if (!resp.ok) {
|
|
const text = await resp.text().catch(() => '');
|
|
throw new Error(`HTTP ${resp.status}: ${text || resp.statusText}`);
|
|
}
|
|
const blob = await resp.blob();
|
|
const disposition = resp.headers.get('Content-Disposition') || '';
|
|
const match = disposition.match(/filename=([^;]+)/);
|
|
const filename = match ? match[1].trim().replace(/^"|"$/g, '') : 'mailfilter-backup.json';
|
|
const downloadUrl = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = downloadUrl;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(downloadUrl);
|
|
const sizeMb = (blob.size / 1024 / 1024).toFixed(2);
|
|
statusDiv.innerHTML = `<small>Backup heruntergeladen (${sizeMb} MB).</small>`;
|
|
} catch (e) {
|
|
statusDiv.innerHTML = `<small style="color:var(--pico-color-red-500, #b00);">Fehler: ${e.message}</small>`;
|
|
} finally {
|
|
btn.removeAttribute('aria-busy');
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
document.getElementById('backup-import-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
if (!confirm('Backup wiederherstellen?\n\nBestehende Konten mit gleichem Benutzernamen/Server werden überschrieben.')) return;
|
|
const formData = new FormData(e.target);
|
|
const resp = await fetch('/api/yaml/backup', {method: 'POST', body: formData});
|
|
const result = await resp.json();
|
|
const div = document.getElementById('backup-result');
|
|
div.style.display = 'block';
|
|
if (result.error) {
|
|
div.innerHTML = `<article role="alert">Fehler: ${result.error}</article>`;
|
|
} else {
|
|
div.innerHTML = `<article>
|
|
Backup wiederhergestellt!<br>
|
|
Konten erstellt: ${result.accounts_created}<br>
|
|
Konten aktualisiert: ${result.accounts_updated}<br>
|
|
Regeln erstellt: ${result.rules_created}<br>
|
|
Verarbeitungsstatus: ${result.processed_restored} Einträge<br>
|
|
Logs wiederhergestellt: ${result.logs_restored ?? 0} Einträge
|
|
</article>`;
|
|
}
|
|
});
|
|
|
|
async function exportYaml() {
|
|
const resp = await fetch('/api/yaml/export');
|
|
const text = await resp.text();
|
|
const pre = document.getElementById('yaml-preview');
|
|
pre.textContent = text;
|
|
pre.style.display = 'block';
|
|
|
|
const blob = new Blob([text], {type: 'text/yaml'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'filters.yaml';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
document.getElementById('yaml-import-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const resp = await fetch('/api/yaml/import', {method: 'POST', body: formData});
|
|
const result = await resp.json();
|
|
const div = document.getElementById('yaml-result');
|
|
div.style.display = 'block';
|
|
if (result.error) {
|
|
div.innerHTML = `<article role="alert">Fehler: ${result.error}</article>`;
|
|
} else {
|
|
div.innerHTML = `<article>
|
|
Import erfolgreich!<br>
|
|
Konten erstellt: ${result.accounts_created}<br>
|
|
Konten aktualisiert: ${result.accounts_updated}<br>
|
|
Regeln erstellt: ${result.rules_created}
|
|
</article>`;
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|