Bisher brauchte es je Konto einen eigenen Kurzbefehl, weil account_id
Pflicht war. Jetzt ist sie optional: Raiffeisen fuehrt die eigene IBAN in der
Datei mit (Treffer gegen die Erkennungs-Texte der Konten), Revolut die
Waehrung – zusammen mit der erkannten Bank und dem Import-Verlauf reicht das.
Bleibt es mehrdeutig, wird NICHT geraten: Ein Auszug im falschen Konto
verfaelscht zwei Salden auf einmal. Stattdessen nennt die Fehlermeldung die
Konten samt Nummern, sodass man sie im Kurzbefehl nachtragen kann. Hat der
Schluessel ein Standardkonto, gilt das.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bisher musste man bei jedem Import Trennzeichen, Datumsformat und
Spaltenzuordnung von Hand eintragen – die haeufigste Fehlerquelle ueberhaupt:
Ein falsches Datumsformat liess alle Zeilen durchfallen, ein falscher
Dezimaltrenner verzehnfachte Betraege.
Zuerst wird gegen die Bank-Vorlagen geprueft (Kopfzeilen-Signatur); alle vier
echten Ausaege des Nutzers werden so sicher erkannt. Sonst inhaltsbasiert:
welche Spalte enthaelt Daten, welche Betraege, welche den laengsten Text.
Zwei Fallen, die dabei aufgefallen sind und getestet werden:
- Deutsche Datumsangaben stecken voller Punkte, und viele Auszuege fuehren
Buchungs- UND Valutadatum. Ohne sie herauszurechnen kaeme "." als
Dezimalzeichen heraus und aus 1.204,70 wuerden 1.20.
- parse_date ist absichtlich nachsichtig; beim Erkennen darf es das nicht
sein, sonst gewinnt ein Format mit Uhrzeit gegen das richtige ohne.
Ganzzahlen ohne Vorzeichen zaehlen nur halb, damit Referenznummern und Kurse
nicht als Betragsspalte durchgehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revolut verschickt fuer eine Zahlung in Euro und eine in Franken dieselbe
Mail vom selben Absender. Ohne die Waehrung als Kriterium landete alles auf
demselben Konto.
Die Erkennung liest die Waehrung ohnehin aus; sie wird jetzt VOR der
Zuordnung ausgewertet und ist ein optionales Filterkriterium. Zuordnungen
mit Waehrung werden zuerst geprueft – wer neben ein allgemeines "revolut"
ein genaues "revolut + EUR" stellt, meint das genaue, unabhaengig von der
Anlagereihenfolge.
Uebergangene Mails melden ausserdem die erkannte Waehrung mit, damit beim
Einrichten sichtbar ist, welcher Wert zu filtern waere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ohne den Beguenstigten ist ein Eintrag im Eingang wertlos – man sieht einen
Betrag und weiss nicht, wofuer. Genau das passierte bei weitergeleiteten
Mails: Gmail zitiert den Text mit "> " davor, die Beschriftung stand also
nicht mehr am Zeilenanfang, und je nach Kodierung kam "Beguenstigter" statt
"Beguenstigter" mit Umlaut an.
Beschriftungen werden jetzt normalisiert verglichen (Umlaute verschriftlicht,
alles ausser Buchstaben und Ziffern verworfen), und Zitatzeichen fallen vor
der Feldsuche weg. Die Suchbegriffe laufen durch dieselbe Normalisierung,
damit sie lesbar bleiben statt doppelt gepflegt zu werden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bei einer von Hand weitergeleiteten Bank-Mail steht die eigene Adresse im
From; "Raiffeisen" findet sich nur noch im Betreff ("WG: Raiffeisen …")
und im zitierten Kopf. Das Feld hiess "Absender enthaelt" und prueft nur die
Absender-Koepfe – die Mail wurde also uebergangen, obwohl das Suchwort
sichtbar darin stand. Eine Falle, kein Bedienfehler.
Gesucht wird jetzt in Absender-Koepfen, Betreff und den ersten 2000 Zeichen
des Textes; die Beschriftung heisst entsprechend "Erkennungstext". Ein Test
mit einer nachgebauten Weiterleitung haelt fest, dass sowohl die Zuordnung
greift als auch Betrag und Empfaenger daraus lesbar bleiben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auf UNSEEN zu filtern klingt sparsam, ist aber eine Falle: Wer eine Mail in
seinem Mailprogramm ansieht, bevor Certain sie geholt hat, verliert sie
dauerhaft – sie ist dann fuer immer gelesen. Genau das ist passiert, als die
Mails vor dem ersten Abruf schon in Gmail geoeffnet waren: Ergebnis "0 neu",
ohne jeden Hinweis auf die Ursache.
Gesucht wird jetzt ab dem letzten Abruf mit einem Tag Ueberlappung; dass
nichts doppelt ankommt, sichert die Message-ID ab.
Die Meldung nennt ausserdem, wie viele Mails ueberhaupt geprueft wurden.
"0 neu" allein liess offen, ob nichts da war oder nichts passte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wer Bank-Mails in ein Sammelpostfach weiterleitet, hat im From je nach
Methode die eigene Adresse stehen. Geprueft werden deshalb auch Reply-To,
Return-Path, Sender und X-Forwarded-For – bei automatischer Weiterleitung
bleibt der Originalabsender dort erhalten.
Ausserdem meldet der Abruf jetzt, welche Mails er uebergangen hat, mit
Absender und Betreff. Beim Einrichten war das sonst Raten: Man sah nur
"3 uebersprungen" und wusste nicht, was in den Filter gehoert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verschluesselung: Bisher gab es nur den Schalter use_ssl – also implizites
TLS auf 993 oder gar nichts. STARTTLS auf 143 fehlte ganz, und das bieten
viele Anbieter an. Jetzt ein Feld mit drei Werten, der Port zieht bei der
Auswahl mit, und "keine" warnt, dass Zugangsdaten im Klartext gehen.
Layout: Die Postfach-Karte steht in einer schmalen Spalte; der waagerechte
Aufbau brach dort auf ein Wort je Zeile um und die Knoepfe lagen ueber dem
Titel. Jetzt gestapelt.
Die Einstellungsseite nutzte ein Raster, das die Karten zeilenweise
ausrichtet – die zweite Reihe begann erst unter der hoechsten Karte der
ersten, weshalb "Backup zurueckladen" allein weit unten stand. Jetzt
fliessen die Karten spaltenweise, und die Spalten sind breiter: vier
schmale Karten nebeneinander waren auf dem Notebook nicht lesbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Verschluesselung fuer Zugangsdaten: Ein IMAP-Passwort laesst sich nicht
hashen, Certain muss es dem Mailserver vorlegen. Es liegt deshalb mit
Fernet verschluesselt in der Datenbank, der Schluessel wird per HKDF aus
session_secret abgeleitet und nirgends gespeichert. Wird das Geheimnis
getauscht, meldet Certain das verstaendlich statt mit einem Stacktrace.
- IMAP-Abruf ueber imaplib/email (Standardbibliothek). Ordnerliste,
Verbindungstest mit klarer Fehlermeldung, Absender-Zuordnung auf Konten,
danach als gelesen markieren oder verschieben.
- Wiedererkennung ueber die Message-ID statt der UID – die vergibt der
Server beim Verschieben neu.
- Nichts wird stillschweigend gebucht: Eine Mail wird zum Eingangs-Eintrag.
Fehlt das Zielkonto oder ist ein Feld unsicher, gilt sie als unklar.
- Hintergrund-Thread beim Start, nach dem Muster des EZB-Kursabrufs: ein
langsames Postfach darf den Dienst nie blockieren.
Getestet gegen einen IMAP-Doppelgaenger, damit die Suite ohne Netz laeuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kein festes Schema je Bank, sondern beschriftete Felder suchen. Die beiden
Banken bauen ihre Mails verschieden: Raiffeisen schickt Klartext mit
"Betrag: 14.90 CHF", Revolut nur HTML mit dem Wert UNTER der Beschriftung.
Beide Bauformen werden abgedeckt, dazu der Betreff als eigene Quelle.
Jeder Wert merkt sich, woher er stammt – ohne das laesst sich ein falscher
Betrag spaeter nicht erklaeren.
Was bewusst NICHT geraten wird: Stehen mehrere gleichwertige Betraege ohne
Beschriftung da (Umsatz und neuer Saldo), bleibt das Feld unklar. Ein
Zahlungsauftrag mit Status "Zur Visierung" gilt nicht als geflossen. Blosse
Zahlen ohne Waehrung sind keine Betraege, und Stilbloecke werden vor dem
Entfernen der Tags herausgeschnitten – sonst haelt das CSS "480px" fuer
Geld.
Die Muster unter tests/fixtures/mail sind echte Mails, anonymisiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Kontoauszug kommt erst am Monatsende; wer im Laden bezahlt hat, weiss
in dem Moment noch, wofuer. iOS laesst keine App fremde Mitteilungen
mitlesen, also fuehrt der Weg ueber einen Kurzbefehl, der die Zahlung an
Certain schickt.
Gebaut:
- Geraetemarken mit eigener Abhaengigkeit statt Sonderfall in
get_current_user. Eine Marke gilt NUR an /quick – auf allen anderen
Wegen ist sie keine Anmeldung. Geht das Telefon verloren, ist kein
Datenabfluss moeglich; Tests halten das fuer Buchungen, Konten,
Kategorien, Export und Datenbanksicherung fest.
- POST /quick/booking laeuft durch detect_rows, damit Schlagwort- und
Betragsregeln die Kategorie schon vorschlagen. external_id verhindert,
dass ein doppelt ausgeloester Kurzbefehl zweimal bucht.
- inbox_items merkt sich die Zahlung ueber das Buchen hinaus. Beim
spaeteren CSV-Import greift der Import-Schluessel nicht: Der Text vom
Telefon ist selten wortgleich mit dem des Auszugs und die Bank bucht ein
bis zwei Tage spaeter. detect_rows vergleicht deshalb zusaetzlich
unscharf – exakter Betrag, Datum +/- 3 Tage, je Eintrag nur einmal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim Kontowechsel musste man die Vorlage jedes Mal neu waehlen – und las
im schlimmsten Fall den Revolut-Auszug mit der Raiffeisen-Vorlage ein
(genau das steht im Verlauf).
Eigene Vorlagen standen schon als template_id im Verlauf; Bank-Vorlagen
sind fest im Code und hinterliessen nichts. Der Verlauf merkt sich deshalb
jetzt auch deren Namen (Migration 0014). Die Oberflaeche waehlt beim
Kontowechsel, womit zuletzt in dieses Konto importiert wurde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Filter steckte in einem zugeklappten Bereich und der Wert musste
blind getippt werden – entsprechend importierte man den Revolut-Auszug
mit allen Produkten in ein Konto, was den Saldo-Abgleich unmoeglich
macht.
Die Rohvorschau liefert jetzt je Spalte die wiederkehrenden Werte samt
Zeilenzahl. Hat eine Datei so eine Spalte, steht der Filter offen sichtbar
mit der Ueberschrift "Diese Datei enthaelt mehrere Konten", und der Wert
wird aus einer Liste gewaehlt ("Giro (530 Zeilen)").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Raten des Gegenkontos war die Quelle fast aller Saldo-Fehler: falsch
geratene Ziele, verdrehte Richtungen und doppelt gebuchte Bewegungen,
sobald beide Ausz-uege eingelesen wurden. Zeilen ohne erkennbares Ziel
blieben ausserdem ungebucht, wodurch Geld unbemerkt fehlte.
Jede Zeile ist jetzt schlicht das, was im Auszug steht: Ausgabe oder
Einnahme. Fruehere Uebertrags-Schlagwoerter haengen stattdessen die
neutrale Kategorie an - der Saldo stimmt, die Statistik bleibt sauber.
Ein Uebertrag laesst sich weiterhin von Hand buchen.
Damit entfallen Gegenbuchungs-Suche, Zielkonto-Raten und der Zustand
"wartet auf Zielkonto".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Erkennungs-Text eines Kontos wurde als blosser Teilstring gesucht.
Am EUR-Konto stand „Euro" – der steckt auch in „PayPal Europe",
„Europa-Park" und „EUROBUS". Jede solche Zahlung wurde dadurch zum
Uebertrag auf dieses Konto, das Geld dort zusaetzlich gutgeschrieben und
der Saldo lief um mehr als tausend Euro daneben.
Gesucht wird jetzt nur an Wortgrenzen; eine Kartenendung wie „6543"
schlaegt damit auch nicht mehr mitten in einer laengeren Kontonummer an.
Das Kontoformular warnt zusaetzlich bei sehr kurzen Erkennungstexten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Stichtag eines Kontos wurde gespeichert und angezeigt, von der
Saldo-Berechnung aber ignoriert: sie addierte jede Buchung des Kontos,
auch die aus der Zeit vor dem Stichtag. Damit lief jeder Saldo um die
gesamte Vorgeschichte daneben.
Der Anfangssaldo beschreibt jetzt den Stand am ENDE des Stichtags – so
steht er auf dem Auszug. Buchungen bis einschliesslich dieses Tages
zaehlen nicht mehr zum Kontostand, bleiben aber in Liste und
Auswertungen. Beim Uebertrag wird jede Seite an ihrem eigenen Stichtag
gemessen; der Vermoegensverlauf folgt derselben Regel. Ohne Stichtag
zaehlt weiterhin alles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kategorien erben ihre Art von der Gruppe, und die ist entweder Ausgabe oder
Einnahme. Für einen Kontoübertrag passt beides nicht: Auf dem einen Konto geht
Geld raus, auf dem anderen kommt es rein – man musste also zwei Kategorien in
zwei Gruppen anlegen.
Als „nicht in Auswertungen einrechnen" markierte Kategorien werden jetzt in
beiden Richtungen angeboten (Buchungsformular und Import-Prüfansicht), und ihre
Schlagwörter greifen unabhängig vom Vorzeichen. Eine Kategorie „Kontoübertrag"
in einer Ausgabe-Gruppe genügt damit für beide Seiten.
Test für beide Richtungen; 94 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Umbuchungen zwischen eigenen Konten sind keine Ausgabe – das Geld ist nicht weg,
es liegt woanders. Bisher blieb nur die Wahl zwischen einer echten Transfer-
Buchung (bildet die Wirklichkeit ab, braucht aber auf beiden Seiten passende
Regeln und schätzt bei Fremdwährung den Gegenwert) oder einer gewöhnlichen
Ausgabe, die Budget und Statistik verfälscht.
Kategorien haben jetzt den Schalter „Nicht in Auswertungen einrechnen": Die
Buchung wirkt weiter auf den Kontostand, bleibt aber aus Budget, Dashboard und
Auswertungen heraus. Gefiltert wird zentral in actual_expense_by_category und
income_by_category – damit greift es zugleich für Budget-Donut, Budget-
Fortschritt, Einnahmen/Ausgaben, Ausgaben-Anteile und die Budget-Seite.
Das ersetzt die Transfer-Paarung nicht, es ist der einfachere Weg: kein
Gegenkonto, keine Kursschätzung, dafür ohne Verbindung zwischen den beiden Seiten.
Migration 0013, Schalter im Kategorie-Dialog, Kennzeichnung in der Liste, Test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Liegen mehrere Währungstausche im selben Zeitfenster, schnappte der zuerst
geprüfte Transfer der eigentlich gemeinten Zeile das Gegenstück weg – diese blieb
dann ohne Gegenkonto stehen. Jetzt gewinnt der Kandidat mit der geringsten
Datums- und Betragsabweichung.
Toleranz von 10 % auf 20 % erhöht: Wir schätzen die Gegenseite mit dem EZB-Kurs,
die Bank rechnet mit ihrem eigenen – und fehlt uns der Kurs, schätzen wir 1:1.
Ein echter Tausch lag damit schnell über 10 % daneben (CHF 150.00 -> EUR 165.60
sind 10,4 %), eine fremde Buchung trifft das Zeitfenster praktisch nie.
An den echten Auszügen: offene Zeilen im Revolut-EUR-Import von 3 auf 1 gesunken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eine Zeile mit „Gegenkonto wählen" wird nicht gebucht, der Abgleich zählte ihren
Betrag aber trotzdem mit und meldete „stimmt überein" – das Geld fehlte im Konto,
ohne dass es auffiel.
Duplikate und Gegenbuchungen zählen weiterhin mit (ihr Betrag steckt bereits in
einer früher gebuchten Buchung); wartende Zeilen zählen jetzt als 0 und tauchen
als erste Abweichung mit Datum und Empfänger auf.
Nachgestellt am echten Raiffeisen-Auszug: Zeigt die Transfer-Regel „Übertrag" auf
das importierte Konto selbst, bleibt die Gutschrift über 92.00 ungebucht; der
Abgleich nennt jetzt genau diese Zeile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spaltenzuordnung aus einem echten Export (Betrag in Spalte „Amount (EUR)",
Partner als Empfänger, Payment Reference als Zweck, Partner-IBAN für die
Transfer-Erkennung). Eine Kontostand-Spalte liefert N26 nicht, dort bleibt der
Saldo-Abgleich also aus.
Nachgerechnet: 246 Zeilen, Summe -719.51; mit Anfangssaldo 872.95 ergibt das
exakt die tatsächlichen 153.44 EUR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Revolut-Auszug enthält ALLE Produkte in einer Datei – im CHF-Export stehen
533 Giro-Zeilen und 255 Zeilen des Ersparten, jedes mit eigenem Kontostand-
Verlauf. Ohne Trennung landeten beide auf demselben Konto und die Pocket-
Bewegungen zählten doppelt.
- Import-Vorlage kennt einen Zeilenfilter („nur Zeilen aus Spalte X mit Wert Y"),
Migration 0012; damit bekommt jedes Pocket sein eigenes Konto
- Gebührenzeilen (Betrag 0.00) können Belastung ODER Erstattung sein – beide
sehen identisch aus. Die Richtung kommt jetzt aus der Kontostand-Spalte;
ohne sie bleibt die Belastung der Normalfall
- Tests für beides; 90 grün
Nachgerechnet an den echten Auszügen: Raiffeisen 223.37, Revolut CHF 61.88,
Erspartes 6.71, Revolut EUR 0.68 – alle vier stimmen zeilengenau mit der
Kontostand-Spalte überein.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die Suche prüfte nur Betrag und Zeitfenster. Ein erwarteter Transfer-EINGANG
passte damit auch auf eine gleich hohe AUSGABE: Die echte Ausgabe galt als
Gegenbuchung, wurde nicht gebucht und der Saldo lief um ihren Betrag zu hoch.
Jetzt muss das Vorzeichen zur Seite des Transfers passen (wo Geld ankam, kann
keine Ausgabe die Gegenseite sein). Test deckt genau diesen Fall ab; 87 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher musste die Regel auf BEIDEN Auszügen ein fremdes Gegenkonto liefern. Beim
Empfangs-Auszug zeigt sie aber auf das Konto, das gerade importiert wird – damit
blieb die Zeile ohne Gegenkonto und wurde still als Einnahme gebucht.
- Gegenbuchungen werden über die Konto-Seite bestehender Transfers gesucht, nicht
über das Konto-Paar. Der Empfangs-Auszug braucht damit keine Regel mehr; eine
Regel auf der abgehenden Seite genügt für beide Dateien.
- Trifft eine Transfer-Regel, ohne dass ein Gegenkonto feststeht, wird die Zeile
nicht mehr als Einnahme/Ausgabe gebucht, sondern in der Prüfansicht mit
„Gegenkonto wählen" markiert – sonst zählt die Bewegung je nach Import-
Reihenfolge doppelt.
- Tests für beide Reihenfolgen; 86 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Revolut tauscht Geld zwischen Währungskonten; im CHF-Auszug steht -100.00 CHF,
im EUR-Auszug +106.42 EUR. Die Gegenbuchungs-Erkennung verglich exakte Beträge
und griff deshalb nie – die Bewegung wurde zweimal gezählt.
- _find_counterpart erkennt die Gegenseite bei unterschiedlichen Währungen über
Konto-Paar, Zeitfenster und einen plausiblen Betrag (10 % Toleranz, weil wir
mit dem EZB-Kurs schätzen und die Bank ihren eigenen nutzt)
- die Gegenbuchung nennt den TATSÄCHLICHEN Betrag: er ersetzt beim Buchen die
Schätzung im bereits gebuchten Transfer, damit beide Salden exakt stimmen
- Test über beide Auszüge: ein Transfer, keine Doppelzählung, EUR-Seite exakt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Revolut-Export führt "2026-01-06 23:11:14" in der Datumsspalte. Wählte man
im Dropdown JJJJ-MM-TT, scheiterte das Parsen an der Uhrzeit – jede Zeile hatte
kein Datum und galt als ungültig ("137 Zeilen erkannt, 0 werden gebucht"),
obwohl die Auswahl richtig aussah.
- parse_date schneidet eine überzählige Uhrzeit ab und kommt auch damit klar,
wenn das Format eine Uhrzeit erwartet, der Wert aber keine hat (ISO-T ebenso)
- Dropdown um die Varianten mit Uhrzeit ergänzt, damit die Bank-Vorlagen ihren
echten Eintrag zeigen statt eines leeren Feldes
- Test: reines Datumsformat auf Datei mit Uhrzeit -> alle Zeilen erkannt, 84 grün
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher merkte man einen Importfehler erst am falschen Endsaldo, ohne Hinweis auf
die Ursache. Enthält der Auszug eine laufende Kontostand-Spalte, gleicht der
Import jetzt jede Zeile dagegen ab: Die Spalte sagt, wie sich der Saldo
tatsächlich verändert hat, unsere Erkennung muss dazu passen.
- neue Spaltenzuordnung "Kontostand (laufend)"; Presets Raiffeisen/Revolut ergänzt
- Prüfansicht zeigt: stimmt überein ODER Differenz + erste abweichende Zeile
(Datum, Empfänger, Betrag laut Bank vs. erkannt)
- meldet zusätzlich den Anfangssaldo laut Datei – häufigste Fehlerquelle
- rechnet mit dem tatsächlich gebuchten Betrag, damit auch nicht gebuchte Zeilen
auffallen (z. B. nicht zugeordnete Gebührenspalte); Fremdwährungs-Zeilen
gehören zu einem anderen Saldo-Verlauf und bleiben außen vor
- erkennt absteigend sortierte Auszüge
- Tests: stimmiger Auszug (ok) und fehlende Gebührenspalte (zeigt genau die
Zeile). 83 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die neue Statusprüfung ließ nur „COMPLETED" durch. Deutsche Bank-Exporte
schreiben aber „ABGESCHLOSSEN", wodurch keine einzige Zeile mehr buchbar war.
Statt einer Positivliste jetzt eine Sperrliste (storniert/ausstehend/abgelehnt/
fehlgeschlagen, jeweils deutsch und englisch); unbekannte Status werden gebucht,
damit keine echten Buchungen still verloren gehen.
Test mit echtem deutschem Revolut-Export inkl. reiner Gebührenzeile
(Betrag 0.00, Gebühr 10.99): Endsaldo trifft die Kontostand-Spalte. 81 grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das Revolut-Preset kannte die Spalten Gebühr/Währung/Status laut Kommentar,
der Parser hat sie aber nie gelesen. Folgen: Gebühren fehlten im Saldo,
stornierte (REVERTED) und schwebende Zeilen wurden mitgebucht, und Zeilen
fremder Währungs-Pockets landeten 1:1 im Konto – der Saldo lief dadurch zu hoch.
- Gebührenspalte mindert den Betrag (immer als Kosten)
- nur Zeilen mit Status COMPLETED (oder ohne Statusspalte) sind buchbar
- Zeilen in anderer Währung als das Zielkonto werden nicht gebucht
- Grund wird in der Prüfansicht als Badge angezeigt (storniert / Währung)
- Gebühr/Währung/Status in der Spaltenzuordnung wählbar; Revolut-Preset ergänzt
- Test mit echtem Revolut-Format (Gebühr, Storno, Fremdwährung), 80 grün
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher wurde eine zweite Buchung mit gleichem Tag/Betrag/Text (z. B. zwei
Gutschriften à 324 am selben Tag) fälschlich als Duplikat verworfen. Jetzt gilt
eine Zeile nur als Duplikat, wenn bereits GENAU SO VIELE identische Buchungen in
der DB stehen – ein erneuter Import derselben Datei bleibt weiterhin Duplikat-sicher.
- csv_import.detect_rows: existing/seen als Counter (Multiset) statt Set
- Tests angepasst + Re-Import-Test ergänzt; an echtem Raiffeisen-Auszug verifiziert
(254 Zeilen, 0 fälschliche Duplikate, Saldo == Bank-Endsaldo)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Enthält der Buchungstext den Erkennungs-Text (Art „Text") eines ANDEREN
Kontos, wird auch ein Eingang als Transfer VON diesem Konto gesetzt – nicht
nur die Ausgabe. Richtung ergibt sich aus dem Vorzeichen.
- Gegenbuchungs-Erkennung: existiert schon ein Transfer zwischen denselben
Konten mit passendem Betrag (±5 Tage), wird die Gegenseite als „Gegenbuchung"
markiert und nicht angehakt – so zählt dieselbe Bewegung beim Import beider
Auszüge nicht doppelt (überschreibbar in der Prüfansicht).
- Tests: Eingang übers Konto-Tag, Gegenbuchung übersprungen (78 grün)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Transfer-Regel kann direkt ein Zielkonto tragen (target_account_id); ohne ->
weiterhin Auto-Erkennung aus dem Buchungstext (Konto-Texte)
- Migration 0010; Endpunkt aktualisiert bestehendes Wort statt Konflikt
- Import bevorzugt das feste Zielkonto, sonst _guess_target_account
- Konten-Seite: Transfer-Erkennung mit Zielkonto-Auswahl + Anzeige am Chip
- Test: festes Zielkonto greift ohne Konto-Texte (75 grün)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Neuer Parser erkennt mehrzeilige Auszüge (N26 & Co.): Beschreibung → Value
Date → Buchungsdatum → Betrag stehen auf eigenen Zeilen; Betrags-Zeile schließt
eine Buchung ab, Kopf-/Fußzeilen (Name/Adresse/eigene IBAN/Seitenzahl/
Spaltenköpfe) werden gefiltert, Gegen-IBAN und Zweck werden übernommen
- Alter Ein-Zeilen-Heuristik-Parser bleibt als Fallback
- /documents/preview liefert zusätzlich die Seiten als PNG-Data-URLs
(PyMuPDF get_pixmap, kein poppler nötig)
- Tests: N26-artige mehrzeilige Fixture + Seiten-Rendering (74 grün);
am echten 34-Seiten-Auszug 207 Buchungen erkannt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CategorizationRule bekommt `kind` (expense|income|transfer); Unique jetzt
(owner, match_field, pattern, kind) statt ohne kind
- Konfliktprüfung/Anlage (add/update Keyword, Transfer-Keyword, gelernte Regeln)
arbeiten kind-bewusst: dasselbe Wort je einmal pro Art möglich
- Migration 0009 mit Backfill (kind aus Kategorie-Art bzw. is_transfer)
- Tests angepasst: quer über Arten erlaubt, gleiche Art weiterhin Konflikt (71 grün)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Zwei-Faktor per TOTP mit zehn Einmal-Wiederherstellungscodes. Der
Zwischenzustand "Passwort stimmt, Code fehlt" haengt an der vorhandenen
Sitzungstabelle (pending_2fa, zehn Minuten Laufzeit) und gilt nirgends als
angemeldet; nach dem Code wird das Cookie getauscht statt weiterverwendet.
Der Wiederverwendungsschutz merkt sich das tatsaechlich getroffene
Zeitfenster, nicht die aktuelle Uhrzeit - sonst liesse sich ein Code aus dem
naechsten Fenster gleich darauf nochmal einloesen.
Passwortrichtlinie (Laenge, Zeichenklassen, MFA-Pflicht) in der neuen
globalen Tabelle app_settings, durchgesetzt beim Anlegen, Zuruecksetzen und
Aendern von Passwoertern. Bestehende Passwoerter lassen sich nicht gegen eine
neue Richtlinie pruefen - der Hash gibt nichts her. Beim Anmelden liegt das
Passwort aber kurz im Klartext vor: dort wird geprueft und bei Verstoss ein
Wechsel verlangt, mit passendem Grund fuer den Hinweistext.
Nebenbei geschlossen: must_change_password war bisher nur im Frontend
erzwungen, wer die API direkt ansprach kam vorbei. enforce_account_state
haengt jetzt am /api-Sammelrouter und laesst nur noch durch, was man zum
Erfuellen der Pflicht braucht.
Das JSON-Backup liess sich bisher grundsaetzlich nicht zurueckladen: keine
Version, Kategorien nur ueber mehrdeutige Namen, sieben Tabellen fehlten.
Daneben steht nun ein Format v2, das Beziehungen ueber lokale IDs abbildet,
je Benutzer oder systemweit. Zurueckgeladen wird nach einer Pruefansicht,
wahlweise ersetzend oder ergaenzend; vor dem Ersetzen legt der Server einen
Schnappschuss der Datenbank an. Der alte Ansichts-Export bleibt fuer
Tabellenkalkulationen bestehen und wird beim Hochladen ausdruecklich
abgewiesen, statt halb zu raten.
Frontend: wiederverwendbares Passwortfeld mit Augensymbol, animierte
Anforderungsliste, 2FA-Seite, Admin-Karte fuer die Richtlinie und Karte zum
Zurueckladen. Neue Bewegung ist in prefers-reduced-motion hinterlegt; Secret
und Codes sind markierbar, sonst verhinderte das globale user-select das
Kopieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regeln:
- Schlagwörter mit optionaler Betragsbedingung (z. B. „SV = 5.00 → Restaurant");
Betrags-Regeln gewinnen vor betragslosen
- Transfer-Schlagwörter (Text → Transfer) auf der Konten-Seite
- Konto-Identifikator-Art „Text": erkennt das Zielkonto eines Transfers aus dem
Buchungstext (längster passender Text gewinnt)
- Migration 0007, Endpunkte, Tests (Betrag/Transfer/Konflikt), gesamt 30 grün
Optik:
- App-Icon: Münzhaufen (aus Nutzer-SVG) weiß auf grünem Grund, klein gehalten
- Farbschema auf pastelliges Grün (Akzent hell/dunkel + Favicon)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Manuelle Schlagwörter je Kategorie (Popup mit Chips, Doppelklick zum Bearbeiten)
- Treffer über Empfänger ODER Verwendungszweck; längstes Schlagwort gewinnt;
Kind-Filter (Ausgabe/Einnahme); wirken nur bei künftigen Importen
- Lernende Korrektur-Erkennung: ab 5 Anwendungen und >= 1/3 Korrekturen
erscheint eine Prüf-Meldung in den Einstellungen (Behalten/Entfernen)
- Neue CategorySelect-Komponente (Baum + Suche, per Portal) ersetzt die
nativen optgroup-Selects in Buchungen und Import-Prüfansicht
- Migration 0006 (source/applied_count/overridden_count/confirmed, match_field any)
- Endpunkte, i18n DE/EN, Tests (test_keywords: 7 neu, gesamt 27 grün)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backup & Export:
- routers/backup: /backup/db (Admin, konsistentes SQLite-Online-Backup),
/export/transactions.csv|json und /export/all.json (owner-gescoped).
- Einstellungen: Bereich „Backup & Export" mit Download-Buttons. docs/BACKUP.md
(Wiederherstellung + LXC-Sicherung). pytest für Export + Admin-Schutz.
Import-Verbesserungen (Nutzer-Feedback zum Ablauf):
- Klarer Hinweis, dass die Zuordnungstabelle nur Beispielzeilen zeigt und
„Vorschau erstellen" die vollständige Prüfliste + Buchen-Button erzeugt.
- Zusammenfassung (erkannt/gebucht) und Warnung, wenn keine gültige Zeile erkannt
wurde (Hinweis auf Format/Bank-Vorlage).
- Fix: Raiffeisen-Preset hat has_header=true (der Export hat eine Kopfzeile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- services/csv_import: PRESETS-Liste (aus echten Beispiel-Exporten des Nutzers),
GET /imports/presets.
- Frontend: Preset-Auswahl im Import füllt Format/Spalten automatisch.
Beide Presets per TestClient gegen echte Zeilenformate verifiziert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>