safe move + sqlite WAL + log indexes + backup with logs
- 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>
This commit is contained in:
+42
-4
@@ -9,7 +9,12 @@
|
||||
<article>
|
||||
<header><h3>Backup erstellen</h3></header>
|
||||
<p>Gesamte Konfiguration als JSON-Datei herunterladen.</p>
|
||||
<button onclick="exportBackup()">Backup herunterladen</button>
|
||||
<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>
|
||||
@@ -47,8 +52,40 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function exportBackup() {
|
||||
window.location.href = '/api/yaml/backup';
|
||||
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) => {
|
||||
@@ -67,7 +104,8 @@ document.getElementById('backup-import-form').addEventListener('submit', async (
|
||||
Konten erstellt: ${result.accounts_created}<br>
|
||||
Konten aktualisiert: ${result.accounts_updated}<br>
|
||||
Regeln erstellt: ${result.rules_created}<br>
|
||||
Verarbeitungsstatus: ${result.processed_restored} Einträge
|
||||
Verarbeitungsstatus: ${result.processed_restored} Einträge<br>
|
||||
Logs wiederhergestellt: ${result.logs_restored ?? 0} Einträge
|
||||
</article>`;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user