automatic DB maintenance: log rotation, orphan cleanup, VACUUM
The existing daily cleanup never effectively ran: it was an interval job counted from container start (reset on every deploy), it called asyncio.get_event_loop() from a worker thread, and even when deleting, SQLite never returns freed pages to the filesystem. Live DB: 4.1 GB, of which 2.6 GB log and 0.6 GB unused pages. New maintenance service, scheduled daily at MAINTENANCE_HOUR_UTC:30 and once 5 minutes after every start: - delete log entries older than LOG_RETENTION_DAYS (30) and keep at most LOG_MAX_ENTRIES (200k) newest ones - delete processed_mails markers of deleted rules/accounts (FKs are not enforced by SQLite) - VACUUM when >25% of the file is unused, only if free disk space covers the copy; never runs twice in parallel GET/POST /api/logs/maintenance expose status and a manual run; the log menu shows DB size, entry count and last run, with "Datenbank aufräumen & verkleinern". Removed the dead cleanup_old_logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+54
-2
@@ -20,9 +20,11 @@
|
||||
<label class="auto-refresh" title="Erste Seite alle 10 Sekunden neu laden">
|
||||
<input type="checkbox" role="switch" id="auto-refresh" onchange="toggleAutoRefresh()"> Live
|
||||
</label>
|
||||
<details class="dropdown head-menu">
|
||||
<details class="dropdown head-menu" ontoggle="if (this.open) loadDbStatus()">
|
||||
<summary>⋯</summary>
|
||||
<ul dir="rtl">
|
||||
<li class="db-status" id="db-status">Datenbank wird geprüft…</li>
|
||||
<li><a href="#" onclick="runMaintenance(); return false;" title="Alte Einträge nach Aufbewahrungsregel löschen und die Datenbankdatei verkleinern">Datenbank aufräumen & verkleinern</a></li>
|
||||
<li><a href="#" onclick="retryFailed(); return false;" title="Löscht den 'verarbeitet'-Marker für Mails mit Abruf-Fehler, damit sie beim nächsten Poll neu versucht werden">Fehlgeschlagene neu einlesen</a></li>
|
||||
<li><a href="#" onclick="clearLogs(); return false;">Log leeren</a></li>
|
||||
</ul>
|
||||
@@ -75,7 +77,9 @@
|
||||
.head-menu { margin:0; }
|
||||
.head-menu summary { padding:0.2rem 0.7rem; height:auto; font-size:1.1rem; line-height:1.2; }
|
||||
.head-menu summary::after { display:none; }
|
||||
.head-menu ul { min-width:230px; font-size:0.9rem; }
|
||||
.head-menu ul { min-width:290px; font-size:0.9rem; }
|
||||
.head-menu li.db-status { direction:ltr; text-align:left; font-size:0.78rem; color:var(--pico-muted-color); line-height:1.5;
|
||||
border-bottom:1px solid var(--app-border, #e3e6ea); padding-bottom:0.5rem; white-space:normal; }
|
||||
|
||||
.toolbar { display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap; margin-bottom:0.4rem; }
|
||||
.search-wrap { position:relative; flex:1 1 360px; }
|
||||
@@ -439,6 +443,54 @@ async function retryFailed() {
|
||||
alert(`${d.unique_uids || 0} Mail(s) neu vorgemerkt. Werden beim nächsten Poll erneut geprüft.`);
|
||||
}
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (n == null) return '?';
|
||||
if (n >= 1e9) return (n / 1e9).toLocaleString('de-DE', {maximumFractionDigits: 2}) + ' GB';
|
||||
return Math.round(n / 1e6).toLocaleString('de-DE') + ' MB';
|
||||
}
|
||||
|
||||
async function loadDbStatus() {
|
||||
const el = document.getElementById('db-status');
|
||||
try {
|
||||
const resp = await fetch('/api/logs/maintenance');
|
||||
const s = await readJson(resp);
|
||||
if (!resp.ok) throw new Error(s.detail || resp.status);
|
||||
const last = s.last_run && s.last_run.started_at
|
||||
? `Letzte Wartung: ${parseUtc(s.last_run.started_at).toLocaleString('de-DE', {dateStyle: 'short', timeStyle: 'short'})}`
|
||||
: 'Wartung läuft täglich automatisch';
|
||||
el.innerHTML = `Datenbank: <b>${fmtBytes(s.file_bytes)}</b> (davon ${fmtBytes(s.free_bytes)} ungenutzt)<br>
|
||||
Log: ${s.log_rows.toLocaleString('de-DE')} Einträge · Aufbewahrung ${s.retention_days} Tage, max. ${s.max_entries.toLocaleString('de-DE')}<br>
|
||||
${esc(last)}`;
|
||||
} catch (e) {
|
||||
el.textContent = 'Status nicht verfügbar: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function runMaintenance() {
|
||||
document.querySelector('.head-menu').open = false;
|
||||
if (!confirm('Datenbank jetzt aufräumen und verkleinern?\n\nAlte Log-Einträge werden nach der Aufbewahrungsregel gelöscht und die Datei wird neu geschrieben. Bei großen Datenbanken kann das einige Minuten dauern; die Mail-Verarbeitung wartet solange.')) return;
|
||||
const stats = document.getElementById('log-stats');
|
||||
const prev = stats.textContent;
|
||||
stats.innerHTML = '<span aria-busy="true">Datenbank wird aufgeräumt…</span>';
|
||||
try {
|
||||
const resp = await fetch('/api/logs/maintenance?vacuum=true', {method: 'POST'});
|
||||
const d = await readJson(resp);
|
||||
if (!resp.ok) throw new Error(d.detail || resp.status);
|
||||
const r = d.result;
|
||||
if (r.skipped) { alert(r.skipped); return; }
|
||||
alert(`Fertig in ${r.duration_s} s.\n\n` +
|
||||
`Größe: ${fmtBytes(r.size_before)} → ${fmtBytes(r.size_after)}\n` +
|
||||
`Gelöschte Log-Einträge: ${(r.logs_deleted_age + r.logs_deleted_cap).toLocaleString('de-DE')}\n` +
|
||||
`Verwaiste Marker entfernt: ${r.processed_orphans_deleted.toLocaleString('de-DE')}\n` +
|
||||
`Verkleinern: ${r.vacuum}`);
|
||||
reload();
|
||||
} catch (e) {
|
||||
alert('Wartung fehlgeschlagen: ' + e.message);
|
||||
} finally {
|
||||
if (stats.querySelector('[aria-busy]')) stats.textContent = prev;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user