Neu drin: wo der einzige 409-Fall sitzt und wie er behandelt wird (eine Frage, kein Fehler), woran man einen Uebertrag im Eingang erkennt, warum die Dublettenliste die Kennung der Bank zeigt, und der Backend-Fehler, den die App ans Licht brachte. Ausserdem der Hinweis, der den naechsten sonst eine Stunde kostet: Die Pruefdatei-Tests zaehlen keine Mengen mehr. Wer die Saat erweitert, macht sonst die aelteren Pruefungen rot, ohne dass etwas kaputt waere. Festgehalten, was weiter bewusst fehlt: Vertragslogo, Kontoreihenfolge und das Loesen einer Uebertrags-Verknuepfung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
555 lines
26 KiB
Markdown
555 lines
26 KiB
Markdown
# Ü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 `9edf610`. 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ä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
|
|
|
|
`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/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, **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_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` ✅
|
|
|
|
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_id` liefert **nichts**, nicht etwa alles. Der Filter
|
|
schlägt auf `TransactionTag.owner_id` an.
|
|
|
|
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 4 ✅
|
|
|
|
### 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
|
|
|
|
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.)
|
|
|
|
### 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:
|
|
|
|
1. **Das Dashboard holt alles in einem Wert** (`Dashboarddaten`). Vier
|
|
getrennte Zwischenspeicher könnten offline aus vier verschiedenen
|
|
Momenten stammen und sich widersprechen.
|
|
2. **Das Nachschlagewerk sitzt im `AppZustand`**, nicht in den Ansichten.
|
|
`Nachschlag` hält Kategorien, Schlagwörter und Konten; jede Liste braucht
|
|
zu einer ID den Namen, das Symbol und die Farbe.
|
|
3. **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 `richtung` der 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
|
|
|
|
1. **Eine Teiländerung schickt nur, was gesetzt ist.** Ginge ein nicht
|
|
angefasstes Feld als `null` hinaus, löschte jedes Speichern den Empfänger,
|
|
die Notiz und die Kategorie gleich mit. Swift macht das von selbst
|
|
(`encodeIfPresent` für Optionals) — der Test dazu bleibt trotzdem stehen,
|
|
die Eigenschaft ist zu wichtig für eine ungeprüfte Spracheigenheit.
|
|
2. **Eine leere Tag-Liste geht sehr wohl mit.** Sie heisst „alle abnehmen" und
|
|
darf nicht mit „nicht angefasst" zusammenfallen.
|
|
3. **Ein Übertrag bekommt keine Kategorie.** Das Geld wechselt nur die Tasche.
|
|
4. **Bei „berechnet" bleibt `planned_cents` unberü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_id` ist 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.
|
|
|
|
#### 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 5 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`) und das **Lösen** einer Übertrags-Verknüpfung
|
|
(`DELETE /uebertraege/{link_id}`) — in der Arbeitsliste gibt es nur noch
|
|
nichts Verknüpftes zu lösen. Alles drei ist am Rechner bequemer und hielt
|
|
niemanden auf.
|
|
|
|
**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.
|
|
Stufe 2 hat rund 1800 Zeilen gebraucht, Stufe 3 rund 1200, Stufe 4 rund 900;
|
|
Stufen 5 und 6 zusammen sind grob eine Woche, Stufe 7 nochmal zwei bis drei.
|
|
|
|
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 in `project.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 10106` im 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 von `requirements.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.
|