docs: Uebergabe fuer die iOS-App, damit der naechste Agent weiterarbeiten kann

Der Plan lag bisher ausserhalb des Repositories und waere beim Geraetewechsel
verloren gewesen. ios/UEBERGABE.md haelt jetzt alles fest: die Entscheidungen
des Nutzers, die Eigenheiten des Backends (Cookie-Anmeldung, Zeitstempel ohne
Zone, Uebertraege in zwei Mechanismen), was Stufe 1 enthaelt, was die drei
offenen Backend-Ergaenzungen sind und was die Stufen 2 bis 7 bringen.

Dazu die Konventionen des Projekts und die Fallen, die mich Zeit gekostet
haben - damit sie niemand zweimal findet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-20 21:53:06 +02:00
co-authored by Claude Opus 5
parent 6b86a6bf91
commit f1b581d213
3 changed files with 372 additions and 0 deletions
+12
View File
@@ -43,3 +43,15 @@ künftigen Importen. Korrektur-Erkennung: ab 5 Anwendungen und ≥ 1/3 Korrektur
Prüf-Meldung in den Einstellungen (Behalten/Entfernen). Auf-/zuklappbares
Kategorie-Dropdown mit Suche (ersetzt das native optgroup-Select) in Buchungen
und Import-Prüfansicht.
## Phase 6 – iOS-App 🚧
Native App in `ios/`, die sich über die Server-Adresse ans selbe Backend hängt.
Anmeldung über eine Gerätemarke der Reichweite `app` (kein Passwort auf dem
Gerät, kein 30-Tage-Ablauf, einzeln widerrufbar). Deutsch und Englisch, iPad
zweispaltig, offline der letzte Stand mit Datum.
Stufe 1 ✅: Gerüst, Netzwerkschicht, Anmeldung samt zweitem Faktor und
Pflicht-Passwortwechsel, Kontenliste. Stufen 2–7 (Lesen, Schreiben,
Arbeitsliste, Nebenräume, Erweiterungen, voller CSV-Import) stehen aus.
**Stand, Entscheidungen und offene Punkte: `ios/UEBERGABE.md`.**
+5
View File
@@ -4,6 +4,11 @@ Native App zum Certain-Backend. Der Code hier ist **nie übersetzt worden** —
der Maschine, auf der er entstanden ist, gibt es kein Xcode. Rechne beim ersten
Bauen mit Fehlern und schick sie mir; das ist eingeplant.
> **Wer hier weiterarbeitet, liest zuerst [UEBERGABE.md](UEBERGABE.md).** Dort
> steht, was entschieden ist, was fertig ist, was als Nächstes dran ist und
> welche Eigenheiten das Backend hat. Dieses README erklärt nur, wie man den
> Code ins Projekt bekommt.
## Projekt anlegen
1. Xcode → neues Projekt → **App**, Schnittstelle **SwiftUI**, Sprache **Swift**.
+355
View File
@@ -0,0 +1,355 @@
# Ü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: 20.10.2026, Commit `6b86a6b`. 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 wichtigste Rahmenbedingung
Auf der Maschine, auf der der Code entsteht, **gibt es kein Xcode**. Der Swift-
Code wird blind geschrieben und nie übersetzt. Der Nutzer legt das Projekt an,
übersetzt und meldet die Fehler zurück.
Daraus folgt die Arbeitsweise: **kleine Stufen, jede für sich übersetzbar.**
Nicht zehntausend Zeilen schreiben und dann einen Berg Fehlermeldungen
hinterlassen. Nach jeder Stufe übersetzt der Nutzer.
---
## 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/`, der Nutzer legt das Xcode-Projekt an |
| 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ährung**
- `amount_eur_cents` — beim Speichern **eingefrorener** EUR-Wert
- `amount_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:
1. **Was ein Übertrag ist.** Entweder `type == "transfer"` **oder** die Buchung
steht in `GET /api/transactions/links`. Es gibt zwei Mechanismen
nebeneinander: ein Übertrag am Stück (eine Buchung mit `to_account_id`) und
die Verknüpfungs-Schicht `TransferLink` (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 nach `paarZu`).
2. **Welche Verträge in `aus_vertraegen_cents` stecken.** Das Backend liefert
nur die Summe; welche Abos dahinterstehen, ermittelt das Frontend selbst aus
`/subscriptions` nach `category_id`.
### Buchungsliste ohne Suche
`GET /api/transactions` kennt nur `account_id`, `category_id`, `uncategorized`,
`type`, `date_from`, `date_to`, `limit` (Standard **5000**), `offset`. **Keine**
Volltextsuche, kein Tag-Filter, keine wählbare Sortierung. Die Weboberfläche
lädt alles und filtert im Browser. Auf dem Telefon ist das spürbar — daher
Backend-Ergänzung B unten.
### Was nicht in die App kommt
- **Bankkonten verbinden.** Die Bank leitet fest auf
`{base_url}/bank/rueckkehr` zurü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/preview` ist eine Einbahnstrasse: Bild rein,
erkannter Text und Buchungsvorschläge raus, die Datei wird nicht gespeichert.
---
## 4. Backend-Ergänzungen
Vier geplant. **A ist fertig**, B bis D stehen aus. Jede 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_session` an
`POST /api/inbox/tokens`). Sonst stellte ein Dieb sich Ersatzschlüssel aus
und das Widerrufen liefe ins Leere.
### B — Suche und Tag-Filter auf `/transactions` (offen)
Drei Query-Parameter: `q` (Volltext über `payee`, `description`, `notes`),
`tag_id`, `subscription_id`. Muster für den Tag-Filter steht schon da: der
`uncategorized`-Zweig in `backend/app/routers/transactions.py` arbeitet mit
genau so einer Unterabfrage.
Nötig für Stufe 2 — ohne das lädt das Telefon für jede Ansicht bis zu 5000
Zeilen.
### C — `GET /budget/template` liefert `modus` und `zuschlag_cents` (offen)
Beide fehlen heute in der Antwort (`backend/app/routers/budgets.py`), obwohl
das Modell sie führt und `PUT` sie zurückgibt. Ohne sie kann die App die
Vorlage weder richtig anzeigen noch bearbeiten. Nötig für Stufe 3.
### D — Zwei Kopier-Lücken im Budget (offen)
`copy_template_to_month` und `template_from_month` in
`backend/app/services/budget.py` übernehmen nur `planned_cents`. Ein
„berechneter" Eintrag fällt beim Kopieren still auf „fest" zurück. Das ist ein
**bestehender Fehler**, kein App-Thema — aber die App macht ihn sichtbar, weil
sie beide Richtungen anbietet. Nötig für Stufe 3.
---
## 5. Was fertig ist — 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 |
### Wie die Anmeldung abläuft
1. Nutzer gibt die Adresse ein → `GET /api/version` prüft, ob dort Certain läuft.
2. Nutzer meldet sich an → normales Cookie, inklusive 2FA und
Pflicht-Passwortwechsel.
3. **Noch in derselben Sitzung** legt die App über `POST /api/inbox/tokens` mit
`scope: "app"` eine Marke an und legt den Klartext in den **Schlüsselbund**.
4. 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.)
### Was nicht geprüft ist
**Ob es übersetzt.** Der Nutzer hatte Stufe 1 beim Schreiben dieser Datei noch
nicht gebaut. Rechne damit, dass seine erste Rückmeldung Fehlermeldungen sind —
das ist eingeplant und kein Rückschlag.
---
## 6. Was als Nächstes dran ist
**Stufe 2 — Lesen.** Dashboard (feste Ansicht: Kontostände, Budget-Fortschritt,
letzte Buchungen, Einnahmen/Ausgaben), Budget-Monatsansicht, Buchungsliste,
Abo-Liste. Dazu Backend-Ergänzungen **B** und **C**.
**Stufe 3 — Schreiben.** Buchungen anlegen und ändern, Budget bearbeiten
(Monat und Vorlage, beide Betriebsarten `fest`/`berechnet`), Abos verwalten
samt Logo, Konten bearbeiten. Dazu Backend-Ergänzung **D**.
**Stufe 4 — Arbeitsliste.** Eingang mit Buchen, Verwerfen, Kategorie zuordnen,
Übertragsvorschlägen und Dubletten. Fachlich der Teil, der am besten aufs
Telefon gehört: eine Zeile, eine Entscheidung.
**Stufe 5 — Nebenräume.** Kategorien, Tags, Schlagwörter, Einstellungen,
Bankabruf anstossen. Hier gehört auch das **Einrichten** des zweiten Faktors
hin — bisher verweist die App dafür in den Browser.
**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.
Stufen 2 bis 6 zusammen sind grob drei bis vier Wochen, Stufe 7 nochmal zwei
bis drei.
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
### Beim Übersetzen am ehesten verdächtig
Nach abnehmender Wahrscheinlichkeit:
1. **`@testable import Certain`** in `Tests/GrundlagenTests.swift` muss auf den
Namen des App-Ziels zeigen.
2. **`Lader` ist eine generische `@Observable`-Klasse** — die ungewöhnlichste
Konstruktion im Code. Wird bisher nur in `KontenListe` benutzt.
3. **Nebenläufigkeit:** `AppZustand` ist `@MainActor`, `CertainClient` ein
Actor. An den Übergängen kann es unter strikter Prüfung Warnungen setzen.
4. **`Tab` und `.tabViewStyle(.sidebarAdaptable)`** setzen iOS 18 voraus — bei
iOS 26 als Mindestversion kein Problem.
5. Die Tests nutzen **Swift Testing** (`import Testing`, `@Test`, `#expect`),
nicht XCTest.
### 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.
- **Tests**: `cd backend && ./.venv/Scripts/python.exe -m pytest tests/ -q`
(Python 3.12 im venv; das System-Python kann nicht bauen). Dauert rund zwei
Minuten — im Hintergrund laufen lassen.
- **Vorbekannt rot**, nicht von dir verursacht:
`test_mail_import.py::test_von_hand_weitergeleitete_mail_wird_am_betreff_erkannt`
und `test_migrations.py::test_batch_migration_keeps_dependent_rows` (der
scheitert an einem Umgebungsproblem, `WinError 10106` in einem Subprozess).
Erwartung nach jedem Lauf: genau diese zwei, sonst nichts.
- **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.