# Ü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 `82db09e`. 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 3 ✅ ### 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. --- ## 6. Was als Nächstes dran ist **Stufe 4 ist dran.** Alle vier Backend-Ergänzungen sind drin; von hier an reicht die API, wie sie ist. Was aus Stufe 3 bewusst nicht mitkam: das **Logo** eines Vertrags (Upload und Zuschnitt, `POST /subscriptions/{id}/logo`) und die **Reihenfolge** der Konten (`PUT /accounts/order`). Beides ist am Rechner bequemer und hielt niemanden auf. **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. Stufe 2 hat rund 1800 Zeilen gebraucht, Stufe 3 rund 1200; Stufen 4 bis 6 zusammen sind grob ein bis zwei Wochen, Stufe 7 nochmal zwei bis drei. Der **409-Fall** („nachfragen", mit `trotzdem=true` wiederholbar) kam in Stufe 3 nicht vor — er steckt nur an `POST /transactions/{id}/transfer`, also in Stufe 4. Dort wird er gebraucht: Die App muss den Hinweis zeigen und den Aufruf auf Wunsch wiederholen, statt den Fehler bloss zu melden. 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 <