redesign filter page: rule test, match stats, warnings, toggle

- Rule tester (POST /api/filters/test): evaluates an unsaved rule
  read-only against the newest matching mails in the source folder,
  using an IMAP SEARCH on the first text condition to narrow candidates.
  Shows matches, near misses with the failing condition, and whether
  any candidate had an attachment at all (would have caught the
  Klarmobil and it-moench misconfigurations immediately).
- Per-rule match_count / last_match_at (new migration), incremented by
  the scheduler on every match and shown in the list.
- PATCH /api/filters/{id}/enabled toggles a rule without resetting its
  processed state (a full PUT re-evaluates every mail for that rule).
- New filter list: cards with enable switch, priority, condition and
  action chips, match stats, and warnings (display name in from/to,
  "am (exakt)" date, duplicates, empty values, invalid regex, move
  without folder). Quick filters for active/inactive/warnings/never
  matched; copy-as-template; remembered account selection.
- Edit dialog: wider layout, live warnings, integrated test panel,
  sensible defaults for new rules (from + date after Jan 1 + move).
- Light global refresh: nav with active item, page background, cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 09:48:12 +02:00
co-authored by Claude Opus 5.5
parent ce7caf5dfe
commit 1b4f6ed34e
9 changed files with 849 additions and 377 deletions
@@ -0,0 +1,31 @@
"""add match statistics to filter_rules
Revision ID: 7d1e4a2b9c30
Revises: 3bccad0c6646
Create Date: 2026-09-29 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "7d1e4a2b9c30"
down_revision: Union[str, Sequence[str], None] = "3bccad0c6646"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Trefferzähler und Zeitpunkt des letzten Treffers pro Regel, für die
Anzeige in der Filterliste."""
with op.batch_alter_table("filter_rules", schema=None) as batch_op:
batch_op.add_column(sa.Column("match_count", sa.Integer(), nullable=False, server_default="0"))
batch_op.add_column(sa.Column("last_match_at", sa.DateTime(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("filter_rules", schema=None) as batch_op:
batch_op.drop_column("last_match_at")
batch_op.drop_column("match_count")