"Fehlgeschlagene neu einlesen" reported the same 29 mails on every
click: it selected all error log entries ever written and never noted
that one had already been retried.
Error log entries now get retried_at when they are picked up; the
endpoint only considers entries without it. If the mail fails again,
the scheduler writes a new, open error entry, so a second click right
after the first reports 0. The log shows retried errors greyed out
with "↻ erneut versucht <Zeit>", and the result dialog says how many
open error entries and mails were requeued.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A spam mail matched stefanhacker@gmx.de but "Aktion fehlgeschlagen"
every time: it has no Message-ID header, and the safe move refused to
proceed because it verifies the copy by Message-ID. So every mail
without that header stayed in INBOX forever, and the log didn't say why.
Verification now falls back when there is no Message-ID:
- COPYUID from the COPY response when the server supports UIDPLUS
- otherwise count mails in the target with the same exact size and
sender before and after COPY; exactly one more means it arrived
The source is still only flagged/expunged after verification, and
move_mail now refuses to run without source_folder (the verification
selects the target, so STORE could otherwise hit the wrong folder).
Checked against a simulated IMAP server for success, rejected COPY,
copy not arriving, and missing source folder.
IMAPClient.last_error carries the reason (rejected COPY with server
text, copy not found incl. how it was checked, missing target folder,
no SMTP config, ...); the scheduler appends it to the log message.
"Fehlgeschlagene neu einlesen" now also retries failed actions, not
only fetch errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The log had 6.45M rows, of which 434 were actual actions: every mail x
every non-matching rule wrote "Keine Übereinstimmung" (5.3M rows, ~1 GB),
plus "keine neuen Mails" per rule every 30s and a "Poll gestartet" row.
Free-text searches took 50-70s.
- Routine entries are only written with LOG_VERBOSE=true; the poll
summary is logged only when there were new mails, actions or errors.
"Why didn't my rule match" is now answered by the rule tester.
- The scheduler commits every 25 evaluated mails and after each rule,
since the dropped log calls used to be the implicit commit points.
- Migration rebuilds filter_logs without the routine rows (live:
6.45M -> ~38k) via copy instead of DELETE.
- /api/logs: keyset pagination (before_id) instead of OFFSET, count
capped at 1000, since/until range, multi-level filter, per-field
filters; ordner: also matches the move target stored in details.
- New log page: one search field with shortcuts (von:, betreff:,
regel:, ordner:, text:), live search with debounce and abort, active
filters as removable chips, click on rule/sender/folder in a row to
filter, time range (default 7 days), level quick filter, expandable
row details, "Ältere laden", filters kept in the URL.
- Disable Pico's colour transition on segmented buttons (they briefly
flashed in the primary colour on load).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fix: "Log leeren" without account filter used a plain DELETE over
millions of rows. That held the sqlite write lock long enough that
the scheduler's own log writes started failing with
"database is locked", and the request itself eventually 500'd back
as HTML "Internal Server Error" — which the frontend then tried to
JSON.parse, producing the popup the user reported. Now uses
DROP+CREATE for full clears (near-instant) and wal_checkpoint to
reclaim space; per-account clear still uses a normal DELETE.
- Raise sqlite busy_timeout from 10s to 60s so a legitimately slow
writer no longer starves smaller ones.
- retry-failed: single bulk DELETE with mail_uid IN (...) per account
instead of one query per uid.
- Both endpoints now return proper HTTPException(500, ...) with detail
instead of letting the exception bubble as plain text.
- Frontend: parseResponse() reads text first, then tries JSON. Both
clearLogs() and retryFailed() surface the actual server message
instead of crashing on non-JSON responses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- fix: fetch_mail crashed on MIME pseudo-encodings like 'unknown-8bit',
'x-unknown', '8bit'. New _safe_decode helper maps those (and any
LookupError from unknown codecs) to latin-1, which never fails on
8-bit input. Used in _extract_body and _decode_header_value.
- Consequence of the crash: scheduler marked the affected UIDs as
processed to avoid retry loops, so those mails never got sorted
even after the underlying issue was fixable.
- feat: POST /api/filters/retry-failed drops the ProcessedMail markers
for UIDs that appear in "Fehler beim Abrufen" error logs, so the
next poll re-evaluates them. Reachable via a button in the log UI
("Fehlgeschlagene neu einlesen"), optionally scoped per account.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>