b77b192b56
- fix: Starlette 1.0 changed TemplateResponse signature — request is now the first positional argument, not nested inside the context dict. All HTML routes returned 500 (unhashable dict in jinja2 template cache) after the image rebuild picked up the new starlette version. - fix: processed_mails (1.7M rows in production: 1 account × 12 rules × 141k inbox mails) made backup export hit 558 MB / 90s. Moved to opt-in checkbox in the UI alongside the logs option, default off. - yield_per for streaming the processed_mails query when included. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
6.7 KiB
HTML
158 lines
6.7 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.25rem;">
|
|
<input type="checkbox" id="backup-include-processed" style="margin-bottom:0;">
|
|
Verarbeitungsstatus mit sichern <small style="opacity:0.7;">(Marker, welche Mails schon verarbeitet wurden — kann sehr groß werden)</small>
|
|
</label>
|
|
<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 includeProcessed = document.getElementById('backup-include-processed').checked;
|
|
const btn = document.getElementById('backup-download-btn');
|
|
const statusDiv = document.getElementById('backup-export-status');
|
|
btn.setAttribute('aria-busy', 'true');
|
|
btn.disabled = true;
|
|
const heavy = includeLogs || includeProcessed;
|
|
statusDiv.innerHTML = `<small>Backup wird erstellt${heavy ? ' (kann dauern...)' : ''}</small>`;
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (includeLogs) params.set('include_logs', 'true');
|
|
if (includeProcessed) params.set('include_processed', 'true');
|
|
const url = `/api/yaml/backup${params.toString() ? '?' + params.toString() : ''}`;
|
|
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 %}
|