Neu drin: warum der zweite Faktor sich hier einrichten, aber nicht abschalten laesst (das verlangt das Kontopasswort, und die App speichert keines - ein Vollzugang auf dem Geraet soll den Schutz nicht aufheben koennen), warum auf dem Telefon die otpauth-Adresse statt eines QR-Codes genommen wird, und dass die Rettungscodes genau einmal erscheinen. Dazu der Grund fuer Einstellungen ohne Sichern-Knopf und der Hinweis, dass sie dem Konto gehoeren, nicht dem Geraet. Die Liste dessen, was bewusst am Rechner bleibt, ist jetzt vollstaendig: Vertragslogo, Kontoreihenfolge, Loesen einer Verknuepfung, Transfer-Schlagwoerter und das Anwenden von Regeln auf Bestehendes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 KiB
Übergabe: iOS-App für Certain
Für den nächsten Agenten. Diese Datei ersetzt die Unterhaltung, in der die App geplant wurde. Sie enthält alles, was du brauchst, ohne sie gelesen zu haben: was entschieden ist, was fertig ist, was als Nächstes dran ist, und was ich über das Backend herausgefunden habe. Lies sie ganz, bevor du anfängst — die Abschnitte „Was das Backend vorgibt" und „Fallen" ersparen dir Tage.
Stand: 21.10.2026, Commit d290930. Alles committet und gepusht.
1. Worum es geht
Certain ist ein Haushaltsbuch: FastAPI-Backend (backend/) plus
React-Weboberfläche (web/), die das Backend mit ausliefert. Dazu kommt eine
native iOS-App in ios/, die sich über eine eingegebene Server-Adresse (wie
budget.scarriffle.com) an dasselbe Backend hängt.
Ziel: alles, was die Weboberfläche kann. Budget, Buchungen, Konten, Abos, Dashboard, Arbeitsliste, Kategorien, Tags, Einstellungen, Bankabruf und der volle CSV-Import.
Die Rahmenbedingung, die es nicht mehr gibt
Bis zum 21.10.2026 stand hier: es gibt kein Xcode, der Swift-Code wird blind
geschrieben und nie übersetzt. Das gilt nicht mehr. Auf dieser Maschine
liegt Xcode 27 mit Simulatoren für iOS 26.5 und 27, und ios/project.yml
erzeugt das Projekt. Übersetzen, Tests und Starten im Simulator gehen hier
direkt:
cd ios && xcodegen generate # nach jeder neuen Datei
xcodebuild -project Certain.xcodeproj -scheme Certain \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' build # oder test
Übersetze nach jeder Stufe selbst, statt dem Nutzer Fehler zu hinterlassen. Die kleinen Stufen bleiben trotzdem richtig — nicht wegen der Fehlersuche, sondern damit jede Stufe für sich benutzbar ist.
2. Entscheidungen des Nutzers
Alle schon gefallen. Nicht erneut fragen.
| Frage | Festgelegt |
|---|---|
| Backend erweitern | Ja, kleine Ergänzungen — jede einzeln vorlegen, bevor du sie schreibst |
| CSV-Import | Voll, wie im Web (trotz Abraten; steht deshalb als letzte Stufe) |
| Zusätzlicher Umfang | Arbeitsliste/Eingang, Kategorien und Tags, Einstellungen, Bankabruf anstossen |
| Dashboard | Feste iOS-Ansicht, kein Raster |
| iOS-Mindestversion | iOS 26 |
| Ablieferung | Flache Ordner unter ios/; das Projekt entsteht seit dem 21.10. aus project.yml |
| Anmeldung | App-Token (im Backend ergänzt, siehe unten) |
| Erweiterungen | Teilen-Menü für Dateien, Kurzbefehle/Siri |
| Sprachen | Deutsch und Englisch |
| iPad | Richtig mitgedacht, zweispaltig |
| Offline | Zuletzt Geladenes mit „Stand von …" |
Der Nutzer will jede Entscheidung selbst treffen. Wenn eine Frage auftaucht, die den Zuschnitt ändert: fragen, nicht annehmen.
3. Was das Backend vorgibt
Die Befunde aus einer gründlichen Durchsicht. Sie bestimmen den ganzen Entwurf.
Anmeldung
Die Web-API kennt ausschliesslich ein Sitzungs-Cookie (get_current_user
in backend/app/deps.py). Kein Bearer, kein Token im Body. Die Sitzung liegt
serverseitig in der Tabelle sessions (kein JWT), hält 30 Tage und
verlängert sich nicht.
Anmeldung ist ein Formular, kein JSON: POST /api/auth/login mit
application/x-www-form-urlencoded, Feldern username und password
(OAuth2PasswordRequestForm). JSON gibt 422.
Zwei-Faktor: Login antwortet {"status": "2fa_required"} und setzt ein
Pending-Cookie mit 10 Minuten Laufzeit. Dann
POST /api/auth/2fa/verify mit {"code": "...", "recovery": false}. Nach
fünf Fehlversuchen ist die Zwischensitzung verbraucht.
Offene Pflichten sperren jeden Endpunkt mit 403 und einem
maschinenlesbaren detail: password_change_required oder
mfa_setup_required. Erreichbar bleiben nur /health, /version,
/auth/me, /auth/logout, /auth/login, /auth/change-password, /auth/2fa.
Alles hängt unter /api
GET /api/version → {"name": "Certain Finance", ...}. Das ist die
Serverprüfung. Wichtig: Jeder unbekannte Pfad ausserhalb /api liefert mit
Status 200 die Weboberfläche aus — nur /api/version kann einen fremden Server
sicher ausschliessen.
Fehlerformat
{"detail": "..."} — aber bei 422 ist detail eine Liste von
Pydantic-Meldungen statt einer Zeichenkette. Netzwerk/APIFehler.swift fängt
beides ab; wer das übersieht, verliert bei jedem Eingabefehler die Meldung.
Statuscodes: 401 nicht angemeldet, 403 verboten oder Pflicht offen, 404 auch
für fremde IDs (bewusst, Mandanten-Isolation), 409 heisst „nachfragen" und
lässt sich oft mit trotzdem=true wiederholen, 400/422 ungültig.
Beträge
Alles sind Cent-Ganzzahlen. Nirgends Fliesskomma (einzige Ausnahme:
QuickBookingIn.amount). Drei Felder an einer Buchung:
amount_cents— in der Kontowährungamount_eur_cents— beim Speichern eingefrorener EUR-Wertamount_base_cents— bei jedem Lesen in die Hauptwährung umgerechnet
Der Client rechnet nichts davon selbst. Kontosalden, Budget-Soll/Ist, Vertrags-Monatswerte, Dashboard-Zahlen kommen alle fertig. Der Client formatiert nur — und rechnet Prozente und „Rest", die es in der API nicht gibt.
Achtung: *_base_cents kann still auf den nativen Wert zurückfallen, wenn kein
Wechselkurs vorliegt. Die ohne_kurs-Listen in den Antworten sagen, wann das
passiert. Wer sie nicht anzeigt, zeigt stillschweigend falsche Summen.
Zeitstempel — die Falle, die mich Zeit gekostet hat
created_at und Konsorten kommen als 2026-10-20T14:18:15.780705 — ohne
Zeitzone, mit sechs Stellen Mikrosekunden. Beide Standard-ISO8601-Leser von
Foundation scheitern daran, weil sie eine Zone verlangen. Modelle/Zeitangaben.swift
hat deshalb einen dritten Leser. Tagesdaten (date, opening_date) sind
dagegen reine "2026-07-14". Zwei getrennte Typen statt einer
Decoder-Strategie — das ist Absicht, siehe Kommentar dort.
Zwei Dinge, die fachlich im Frontend sitzen
Die App muss sie nachbauen, das Backend hilft nicht:
- Was ein Übertrag ist. Entweder
type == "transfer"oder die Buchung steht inGET /api/transactions/links. Es gibt zwei Mechanismen nebeneinander: ein Übertrag am Stück (eine Buchung mitto_account_id) und die Verknüpfungs-SchichtTransferLink(zwei getrennte Buchungen, wie die Bank sie liefert; keine davon wird verändert). Das Zusammenfassen beider Beine zu einer Anzeigezeile macht heute nur der Browser (web/src/pages/Transactions.jsx, Suche nachpaarZu). - Welche Verträge in
aus_vertraegen_centsstecken. Das Backend liefert nur die Summe; welche Abos dahinterstehen, ermittelt das Frontend selbst aus/subscriptionsnachcategory_id.
Buchungsliste
GET /api/transactions kennt account_id, category_id, uncategorized,
type, date_from, date_to, limit (Standard 5000), offset und seit
Ergänzung B auch q, tag_id, subscription_id. Keine wählbare
Sortierung — immer Datum absteigend, dann ID absteigend.
Die Weboberfläche lädt weiter alles und filtert im Browser; sie wurde nicht umgestellt, weil sie so funktioniert. Die App soll die Filter benutzen.
Was nicht in die App kommt
- Bankkonten verbinden. Die Bank leitet fest auf
{base_url}/bank/rueckkehrzurück, registriert bei Enable Banking. Abrufen, Salden holen und Konten zuordnen läuft nativ — nur das Herstellen nicht. - Postfach einrichten (IMAP-Zugangsdaten) und Sicherung/Wiederherstellung (ersetzt die ganze Datenbank). Gehört an den Rechner.
- Benutzerverwaltung (
/users,/policy) — Administration, weggelassen, solange der Nutzer nicht widerspricht. - Belege als Anhang gibt es nicht. Im Datenmodell existiert nichts
dergleichen.
POST /api/documents/previewist eine Einbahnstrasse: Bild rein, erkannter Text und Buchungsvorschläge raus, die Datei wird nicht gespeichert.
4. Backend-Ergänzungen
Vier geplant, alle vier sind fertig. Falls du eine fünfte brauchst: einzeln dem Nutzer vorlegen, bevor du sie schreibst.
A — App-Token ✅ (Commit 8a7545b)
TokenScope hat neben quick_add jetzt app. get_current_user akzeptiert
den Header X-Certain-Token, wenn die Marke diese Reichweite hat. Kein
Migrationsschritt nötig — _enum() legt Enums ohne DB-Bedingung ab.
Die Prüfung sitzt in _session_user (backend/app/deps.py), nicht in
get_current_user — damit auch enforce_account_state die Marke sieht und
eine App nicht an der Passwortwechsel-Pflicht vorbeikommt.
Zwei Dinge, die die Tests festhalten (backend/tests/test_app_marke.py,
7 Stück):
- Eine
quick_add-Marke ist nicht still zum Vollzugang geworden. Sie gilt weiter nur an den drei/quick-Endpunkten. - Eine App-Marke darf keine weitere ausstellen (
require_cookie_sessionanPOST /api/inbox/tokens). Sonst stellte ein Dieb sich Ersatzschlüssel aus und das Widerrufen liefe ins Leere.
B — Suche und Tag-Filter auf /transactions ✅
Drei Query-Parameter: q (Volltext über payee, description, notes),
tag_id, subscription_id. Der Tag-Filter arbeitet mit einer Unterabfrage
wie der uncategorized-Zweig daneben, nicht mit einem Join — sonst käme eine
Buchung so oft zurück, wie sie Tags trägt.
Zwei Dinge, die die Tests festhalten (backend/tests/test_buchungssuche.py,
7 Stück):
%und_im Suchtext sind Zeichen, keine Platzhalter. Sonst fände die Eingabe „50%" alles.- Ein fremder
tag_idliefert nichts, nicht etwa alles. Der Filter schlägt aufTransactionTag.owner_idan.
Grenze: SQLite vergleicht bei LIKE nur ASCII ohne Rücksicht auf Gross- und Kleinschreibung. „Müller" findet „müller" nicht. Reicht, bis jemand es meldet.
Der lokale Name q im Router hiess vorher die Abfrage selbst; die heisst
jetzt abfrage, damit der Query-Parameter q heissen kann.
C — GET /budget/template liefert modus und zuschlag_cents ✅
Beide fehlten in der Antwort (backend/app/routers/budgets.py), obwohl das
Modell sie führt und PUT sie zurückgibt. Jetzt stehen sie in jeder Zeile.
Test: test_vorlage_nennt_betriebsart_und_zuschlag in test_budget.py.
Das war der stillere der beiden Fehler: Ein Client, der die Vorlage so liest, zeigt eine berechnete Zeile als feste und schreibt sie beim nächsten Speichern auch als feste zurück. Die Weboberfläche traf es nicht, weil sie die Betriebsart aus der Monatsansicht nimmt.
D — Zwei Kopier-Lücken im Budget ✅
copy_template_to_month und copy_month_to_template in
backend/app/services/budget.py übernahmen nur planned_cents. Ein
„berechneter" Eintrag fiel beim Kopieren still auf „fest" zurück — mit der
Zahl, die zufällig zuletzt in planned_cents stand.
ensure_customized nebenan machte es längst richtig; die zwei Kopierer ziehen
jetzt nach. Dabei kam eine Frage auf, die vorher niemand stellen musste: Eine
berechnete Zeile gehört auch mit planned_cents == 0 in die Vorlage, ihre
Zahl steht ja in den Verträgen. Eine feste Null bleibt draussen, sonst
füllte sich die Vorlage mit Nullzeilen für jede je berührte Kategorie.
Drei Tests in test_budget.py halten beide Richtungen und die Null fest.
5. Was fertig ist — Stufen 1 bis 5 ✅
Stufe 1 (Commit 6b86a6b)
21 Swift-Dateien, rund 2600 Zeilen, in ios/:
| Ordner | Inhalt |
|---|---|
App/ |
CertainApp (Einstieg), AppZustand (zentraler Zustand), RootAnsicht (Navigation) |
Netzwerk/ |
CertainClient (Actor um URLSession), Endpunkte, APIFehler, Ablage (Voreinstellungen + Schlüsselbund), Zwischenspeicher (offline), Lader |
Modelle/ |
Grundmodelle (Benutzer, Marke, Einstellungen, Serverkennung), Konto, Zeitangaben |
Bausteine/ |
Geldbetrag, Darstellung (Farben, Symbolzuordnung, Plakette), Zustandsansichten |
Anmeldung/ |
Server-Adresse, Anmeldung, zweiter Faktor, Passwortwechsel |
Konten/ |
KontenListe — die erste Fachansicht |
Einstellungen/ |
Platzhalter: zeigt den Zugang und lässt abmelden |
Texte/ |
de.lproj und en.lproj, zeilengleich |
Tests/ |
GrundlagenTests — reine Logik, Swift Testing, 28 Stück |
project.yml |
XcodeGen-Spezifikation, erzeugt Certain.xcodeproj |
Wie die Anmeldung abläuft
- Nutzer gibt die Adresse ein →
GET /api/versionprüft, ob dort Certain läuft. - Nutzer meldet sich an → normales Cookie, inklusive 2FA und Pflicht-Passwortwechsel.
- Noch in derselben Sitzung legt die App über
POST /api/inbox/tokensmitscope: "app"eine Marke an und legt den Klartext in den Schlüsselbund. - Danach trägt nur noch der Header. Das Cookie wird weggeworfen
(
vergissCookies()).
Kein Passwort auf dem Gerät, kein Ablauf nach 30 Tagen, einzeln widerrufbar.
Was geprüft ist
Jedes Modellfeld wurde gegen echte Antworten eines laufenden Backends
verglichen (/api/version, /api/auth/me, /api/accounts, /api/settings,
/api/inbox/tokens). Alle Namen stimmen. Mach das bei jeder weiteren Stufe
genauso — das ist die einzige Prüfung, die beim blinden Schreiben wirklich
etwas fängt. Wie: Ein kleines Python-Skript mit TestClient gegen eine
Wegwerf-Datenbank, Felder flach ausgeben, mit den CodingKeys vergleichen.
(Das Passwort der Dev-Datenbank unter backend/data/app.db ist unbekannt —
nicht raten, eine frische Testdatenbank nehmen.)
Dass es übersetzt, ist inzwischen geprüft
Am 21.10.2026 gebaut, getestet und gestartet (Commit ffe1870). Genau ein
Übersetzungsfehler in 2600 blind geschriebenen Zeilen: der Vorgabewert
[String: String] = [] in CertainClient.schickeDatei musste [:] heissen.
Sonst keine Fehler, keine Warnung im eigenen Code. Die 28 Tests laufen durch,
die App startet im Simulator bei der Serverabfrage.
Noch nicht geprüft: der Weg gegen ein echtes Backend. Anmelden, zweiter Faktor, Marke anlegen, Kontenliste — das lief bisher nur gegen die Modelle, nicht gegen einen laufenden Server. Siehe „Klartext-HTTP" unten.
Stufe 2 — Lesen ✅ (Commit bf437b3)
Vier neue Reiter, dazu die Modelle und Endpunkte darunter:
| Ordner | Inhalt |
|---|---|
Dashboard/ |
DashboardAnsicht — vier feste Karten, ein Abruf |
Budget/ |
BudgetAnsicht — Soll/Ist je Gruppe, blätterbar |
Buchungen/ |
BuchungsListe — Suche und Filter im Server |
Abos/ |
AboListe — Monats- und Jahressumme, Fälligkeit |
Modelle/ |
Buchung, Budget, Abo, Kategorie, Dashboard |
Drei Entscheidungen, die man kennen sollte, bevor man daran weiterbaut:
- Das Dashboard holt alles in einem Wert (
Dashboarddaten). Vier getrennte Zwischenspeicher könnten offline aus vier verschiedenen Momenten stammen und sich widersprechen. - Das Nachschlagewerk sitzt im
AppZustand, nicht in den Ansichten.Nachschlaghält Kategorien, Schlagwörter und Konten; jede Liste braucht zu einer ID den Namen, das Symbol und die Farbe. - Die Buchungsliste sucht erst beim Absenden. Je Tastendruck zu fragen hiesse, bei „Supermarkt" zehn Abrufe zu schicken, von denen neun niemanden interessieren.
Die Übertrags-Paarung steckt in Buchungszeile.zeilen (Modelle/Buchung.swift).
Zwei Dinge daran sind leicht falsch zu machen, beide durch Tests festgehalten:
- Zusammengefasst wird nur, wenn beide Beine in der Liste stehen. Sonst liesse ein Kontofilter Zeilen verschwinden, die zu diesem Konto gehören.
- Die Richtung kommt aus
richtungder Verknüpfung, nicht aus dem Vorzeichen — manche Banken liefern beide Beine positiv.
Die Modellprüfung ist jetzt ein Test
backend/scripts/erzeuge_ios_fixture.py legt eine Wegwerfdatenbank an,
befüllt sie so, dass jedes Feld einmal belegt ist, und schreibt 15 echte
Antworten nach ios/Tests/Fixtures/antworten.json. Der Swift-Test
Serverantworten entschlüsselt daraus jedes Modell.
Das ersetzt die Handarbeit, die hier früher beschrieben stand. Nach jeder Änderung an einem Schema das Skript neu laufen lassen und die Datei mit committen:
cd backend && .venv/bin/python -m scripts.erzeuge_ios_fixture
Wer ein Modell ergänzt, nimmt seinen Endpunkt in PFADE auf und schreibt eine
Zeile im Test dazu. Das ist die einzige Prüfung, die einen vertippten
Feldnamen fängt — der übersetzt anstandslos.
Stufe 3 — Schreiben ✅ (Commit 82db09e)
Buchungen, Budget-Zeilen, Verträge und Konten lassen sich anlegen, ändern und löschen.
| Datei | Inhalt |
|---|---|
Modelle/Eingaben.swift |
Was zum Server hinausgeht |
Bausteine/Formular.swift |
Speichervorgang, Betragsfeld, Auswahlfelder |
Buchungen/BuchungFormular.swift |
Anlegen und Ändern in einer Maske |
Budget/BudgetZeileBearbeiten.swift |
Monat oder Vorlage, dieselbe Maske |
Abos/AboFormular.swift, Konten/KontoFormular.swift |
ebenso |
Ein Formular je Sache, nicht zwei. Anlegen und Ändern teilen sich die
Maske; was sich unterscheidet, steht in einem Zweck-Typ. Zwei Masken
nebeneinander wären genau die Stelle, an der sie auseinanderlaufen.
Vier Regeln, die man nicht sieht, aber merkt, wenn sie fehlen
- Eine Teiländerung schickt nur, was gesetzt ist. Ginge ein nicht
angefasstes Feld als
nullhinaus, löschte jedes Speichern den Empfänger, die Notiz und die Kategorie gleich mit. Swift macht das von selbst (encodeIfPresentfür Optionals) — der Test dazu bleibt trotzdem stehen, die Eigenschaft ist zu wichtig für eine ungeprüfte Spracheigenheit. - Eine leere Tag-Liste geht sehr wohl mit. Sie heisst „alle abnehmen" und darf nicht mit „nicht angefasst" zusammenfallen.
- Ein Übertrag bekommt keine Kategorie. Das Geld wechselt nur die Tasche.
- Bei „berechnet" bleibt
planned_centsunberührt. Das Backend liest es dann nicht, und es zu überschreiben verlöre den Wert, auf den ein Zurückschalten auf „fest" zurückfallen soll.
Die Währung eines bestehenden Kontos lässt sich nicht ändern — jeder gespeicherte Betrag würde danach etwas anderes bedeuten. Das Backend erlaubt es; die App nicht.
Fehlende Texte findet dieser Befehl
String(localized:) mit unbekanntem Schlüssel übersetzt anstandslos und zeigt
dem Nutzer den Schlüssel. Vor dem Committen:
cd ios && comm -23 \
<(grep -rhoE 'localized: "[a-zA-Z0-9._]+"' --include="*.swift" . | sed 's/localized: "//; s/"//' | sort -u) \
<(grep -oE '^"[a-zA-Z0-9._]+"' Texte/de.lproj/Localizable.strings | tr -d '"' | sort -u)
Gibt er nichts aus, ist nichts offen. (Ein Schlüssel, der nur in einem
? : steckt, taucht fälschlich als „fehlend" nicht auf — das ist die eine
Lücke des Befehls.) Dass beide Sprachfassungen dieselben Schlüssel tragen,
prüft ein diff der beiden grep-Ausgaben.
Stufe 4 — Arbeitsliste ✅ (Commit 9edf610)
Ein Reiter „Eingang" mit vier Abschnitten, jeder mit Anzahl im Kopf:
offene Eingangseinträge, Übertragsvorschläge, Gebuchtes ohne Kategorie,
Dubletten. Arbeitsliste/ArbeitslisteAnsicht.swift, dazu zwei schmale
Auswahlblätter in AuswahlBlaetter.swift.
Alles in einem Abruf (Arbeitsstand), aus demselben Grund wie beim
Dashboard: Vier getrennte Zwischenspeicher könnten offline aus vier
verschiedenen Momenten stammen.
Der 409-Fall sitzt hier, und nur hier
POST /inbox/{id}/transfer kann mit 409 antworten. Das ist eine Frage,
kein Fehler: Die App zeigt den Hinweis und wiederholt den Aufruf auf Wunsch
mit trotzdem: true. bucheAlsUebertrag fängt APIFehler.konflikt eigens
ab, statt ihn in den allgemeinen Fehlerweg laufen zu lassen. Wer später einen
weiteren 409-Endpunkt anbindet, macht es genauso.
Kleinigkeiten, die man sonst zweimal findet
- Ohne Konto lässt sich nichts buchen (400 vom Backend). Der Knopf bleibt grau — und die Zeile sagt „ohne Konto", damit das nicht unerklärt wirkt.
- Ein Übertrag braucht keine Kategorie. Daran erkennt man ihn:
transfer_account_idist gesetzt. Ohne diese Prüfung fragte die Liste ewig nach einer Kategorie, die es nie geben wird. - Die Dublettenliste zeigt die Kennung der Bank. Verschiedene Kennungen heissen: Die Bank hat sie ausdrücklich als verschiedene Zahlungen gemeldet. Ohne diese Spalte lässt sich die Frage nicht entscheiden.
Ein Backend-Fehler, den die App ans Licht brachte
/uebertraege/vorschlaege lieferte jedes Paar zweimal, einmal je
Richtung: Was verknüpft wird, fällt über verbraucht heraus — ein blosser
Vorschlag nicht. Die Weboberfläche zeigte den Doppeleintrag genauso.
Behoben in services/uebertraege.py (schlage_vor), mit Test.
Stufe 5 — Nebenräume ✅ (Commit d290930)
Kategorien, Schlagwörter, Bankabruf und der zweite Faktor — alle unter
Einstellungen, nicht als eigene Reiter. Man braucht sie selten, und eine
Leiste mit zehn Reitern findet niemand mehr. Nebenraeume/.
Was hier bewusst nicht geht
- Den zweiten Faktor abschalten. Es verlangt das Kontopasswort, und die App speichert keines. Ein Vollzugang auf diesem Gerät soll den Schutz des Kontos nicht aufheben können. Dasselbe gilt für neue Rettungscodes.
- Eine Bank verbinden. Die Bank leitet fest auf
{base_url}/bank/rueckkehrzurück; der Weg führt zwangsläufig über den Browser. Abrufen und Salden auffrischen gehen von hier.
Zwei Eigenheiten, die beim Nachbauen zählen
- Der zweite Faktor braucht auf dem Telefon keinen QR-Code. Die
Authenticator-App liegt daneben —
otpauth_uriführt per Link direkt hinein.qr_pngbleibt ungenutzt; auf dem iPad könnte man es zeigen. - Die Rettungscodes erscheinen genau einmal. Das Blatt lässt sich nicht
wegwischen (
interactiveDismissDisabled) und erst schliessen, wenn der Nutzer bestätigt hat, sie gesichert zu haben. Wer es verliert, braucht das Kontopasswort am Rechner.
Einstellungen schreiben ohne Sichern-Knopf
PUT /settings/{key} nimmt je Aufruf einen Wert, als Query. Die Bindung
schreibt beim Setzen direkt; ein Formular mit Sichern-Knopf wäre eine
Zwischenstufe, die nichts bringt. Die Werte gehören dem Konto, nicht dem
Gerät — das steht auch unter dem Abschnitt, sonst wundert sich jemand, warum
der Browser sich mitändert.
Prüfdatei-Tests zählen keine Mengen mehr
Die Saat in erzeuge_ios_fixture.py wuchs um Eingangseintrag, Dubletten und
ein Übertragspaar — und die älteren Tests wurden rot, weil sie exakte Anzahlen
prüften. Sie prüfen jetzt Eigenschaften (>= 2, „enthält einen Übertrag"),
damit sie das nächste Wachstum überleben. Daran denken, wenn du die Saat
erweiterst.
6. Was als Nächstes dran ist
Stufe 6 ist dran. Alle vier Backend-Ergänzungen sind drin; von hier an reicht die API, wie sie ist.
Was bewusst nicht mitkam: das Logo eines Vertrags (Upload und Zuschnitt,
POST /subscriptions/{id}/logo), die Reihenfolge der Konten
(PUT /accounts/order), das Lösen einer Übertrags-Verknüpfung
(DELETE /uebertraege/{link_id}), die Transfer-Schlagwörter
(/categories/transfer-keywords) und das Anwenden von Regeln auf
Bestehendes (POST /categories/keywords/anwenden). Alles am Rechner
bequemer, nichts davon hielt jemanden auf.
Weiter aussen vor, wie von Anfang an entschieden: Bank verbinden, Postfach einrichten, Sicherung und Wiederherstellung, Benutzerverwaltung.
Stufe 6 — Erweiterungen. Teilen-Menü und Kurzbefehle. Brauchen eigene Ziele
im Xcode-Projekt, eine App-Gruppe und eine geteilte
Schlüsselbund-Gruppe; Netzwerk/Ablage.swift hat dafür schon die beiden
Konstanten gruppe und schluesselbundGruppe (stehen auf nil). Für die
Kurzbefehle ist POST /api/quick/booking fertig: Header-Marke, idempotent über
external_id, fertig formulierte Antwort-Meldung.
Stufe 7 — Import, voll. Erkennen, Rohansicht, Spaltenzuordnung, Formateinstellungen, Zeilenfilter, editierbare Prüfansicht, Saldo-Abgleich, Vorlagen, Verlauf mit Rücknahme.
Zum Umfang, ehrlich
Die Weboberfläche hat rund 8900 Zeilen JSX. Nativ wird das nicht weniger. Stufe 2 hat rund 1800 Zeilen gebraucht, Stufe 3 rund 1200, Stufe 4 rund 900, Stufe 5 rund 1500; Stufe 6 ist überschaubar, Stufe 7 nochmal zwei bis drei Wochen.
Der 409-Fall („nachfragen", mit trotzdem=true wiederholbar) ist in
Stufe 4 angekommen und erledigt — siehe dort. In den Stufen 5 bis 7 kommt er
nicht mehr vor.
Stufe 7 steht bewusst am Ende. Alles davor ist für sich nutzbar. Der Ablauf
hat keinen Server-Zustand zwischen den Schritten: Die Datei wird bei jedem
Schritt neu hochgeladen, und beim Buchen gehen die Vorschauzeilen unverändert
zurück an den Server. Eine editierbare Prüftabelle mit 300 Zeilen und neun
Spaltenzuordnungen ist auf Telefonbreite ein Entwurfsproblem, kein
Übersetzungsproblem. Das Backend selbst sagt dazu in
backend/app/routers/quick.py: „Ein Auszug hat schnell hunderte Zeilen; die
am Telefon durchzusehen wäre keine Erleichterung." Der Nutzer kennt diesen
Einwand und will den vollen Import trotzdem.
7. Fallen und Arbeitsweise
Klartext-HTTP — der nächste Stolperstein
Das Projekt läuft mit der Voreinstellung von App Transport Security, also
nur https. Ein selbstgehosteter Server im Heimnetz unter http://… wird
abgewiesen, und http://localhost beim Entwickeln ebenso — obwohl
ServerAnsicht http ausdrücklich zulässt und ein Test das festhält
(„http bleibt http"). Wer gegen ein lokales Backend prüfen will, braucht
in project.yml eine ATS-Ausnahme. Das ist eine Sicherheitsentscheidung des
Nutzers: über Klartext geht die Gerätemarke, ein Vollzugang, mit jeder
Anfrage mit.
Was beim Übersetzen auffiel
Alles, was hier als verdächtig stand, hat gehalten: Lader als generische
@Observable-Klasse, der @MainActor/Actor-Übergang zwischen AppZustand
und CertainClient, Tab mit .tabViewStyle(.sidebarAdaptable), Swift
Testing statt XCTest. Eine Einschränkung: Das Projekt baut in Sprachfassung
Swift 5. Unter Swift 6 greift die strikte Nebenläufigkeitsprüfung, und die
Übergänge sind darauf nicht geschrieben — wer umstellen will, rechnet mit
Arbeit an genau diesen Stellen.
Schreibende Aufrufe mit Query statt Körper
Leicht zu übersehen, das Backend macht es an mehreren Stellen:
PUT /budget/template/category/{id}, PUT /budget/{label}/category/{id},
PUT /settings/{key}, POST /subscriptions/{id}/preis.
Konventionen des Projekts
- Alles auf Deutsch: Code-Kommentare, Bezeichner, Testnamen, Commit-
Meldungen, Oberflächentexte. Testnamen sind deutsche Aussagesätze,
ASCII-transliteriert (
test_app_marke_darf_lesen_und_schreiben). - Commits:
typ: Wirkung für den Nutzer, auf Deutsch, eine Wirkung je Commit. Fertige Arbeit wird committet und gepusht, ohne Rückfrage. - Kommentare erklären das Warum, nicht das Was — und zwar das, was der Code nicht zeigen kann. Das ganze Projekt ist so geschrieben; halte es durch.
- Neue Swift-Datei? Danach
cd ios && xcodegen generate. Die Ordner stehen einzeln inproject.yml; ein neuer Ordner will dort eine Zeile. - Tests:
cd backend && .venv/bin/python -m pytest tests/ -q(auf Windows./.venv/Scripts/python.exe). Python 3.12 im venv; das System-Python kann nicht bauen. Dauert hier rund 45 Sekunden. - Erwartung: alles grün, 438 Tests. Die zwei Ausfälle, die hier früher als
„vorbekannt rot" standen, waren ein Windows-Problem der alten Maschine
(
WinError 10106im Subprozess) und laufen auf diesem Rechner durch. Wer einen roten Test sieht, hat ihn verursacht. - Fehlt dem venv ein Modul (
ModuleNotFoundError), einmal.venv/bin/python -m pip install -r requirements.txt— es war hier nicht auf dem Stand vonrequirements.txt. - Heredocs in Bash scheitern an den deutschen Anführungszeichen („ ").
Für Dateien mit solchen Zeichen das Write-Werkzeug nehmen, nicht
cat <<EOF.
Offene Punkte, die der Nutzer bestimmt
- Name, Bundle-ID und Signierung — er legt das Projekt an.
- Ob die Benutzerverwaltung doch mit hinein soll.
- Ob eine neu vergebene App-Marke am Rechner bestätigt werden muss, bevor sie gilt. Sicherer, aber umständlich beim Einrichten eines neuen Geräts.