Frontend: Abstuerze, stiller Datenverlust und unerreichbare Features beheben #59

Merged
Aceless merged 7 commits from frontend-improvements into main 2026-09-09 22:04:45 +00:00
Owner

Behebt die Frontend-Issues #1 bis #29 und #31 aus dem Review. #30 bleibt offen: die Unit-Tests sind da, Widget-Tests und CI fehlen noch.

Nur app/ ist angefasst. Im Working Tree lagen parallel fremde Backend-Änderungen, die bewusst nicht Teil dieses Branches sind.

Absturz und Datenverlust

  • App-Start hing (#1): Die Versionsprüfung lief vor runApp() gegen zwei Requests ohne Timeout. Bei nicht erreichbarem Server blieb die App minutenlang auf dem Splash. Sie läuft jetzt über appUpdateProvider nach dem ersten Frame und fängt Fehler selbst ab.
  • Rezept speichern stürzte ab (#4): int.parse auf drei Freitextfelder warf bei 1,5 eine FormatException. Jetzt Ziffernfilter, numerischer Validator und tryParse.
  • Rezept speichern verlor Daten (#5): addRecipe wurde nicht awaited, der Screen schloss sofort, Serverfehler gingen als unbehandelte Async-Exception verloren. Jetzt awaited, mit Fehlermeldung, gepoppt nur bei Erfolg.
  • Filter ließen sich nicht abwählen (#6): In copyWith hieß null "nicht ändern". Neuer unset-Sentinel in lib/util/copy_with.dart, angewandt auf RecipeState, MealPlanState, SpacesState und AuthState.
  • Kalenderwoche war falsch (#8): Die alte Formel lieferte für den 01.01.2026 "KW 0". isoWeekNumber rechnet jetzt in UTC über den Donnerstag der Woche.
  • RangeError bei leerem Rezepttitel (#24) und fehlendes catch im Vorschläge-Screen.

Zwei fertige Features waren unerreichbar

  • SettingsScreen (#11) wurde nirgends navigiert. Damit war die gesamte Bring!-Konfiguration nicht bedienbar, obwohl die Detailseite einen "Zu Bring! senden"-Button hat. Jetzt über ein Icon im Wochenplan-Header erreichbar.
  • SharedLinkImportHandler (#12) wurde nie instanziiert. Der Router liefert jetzt rootNavigatorKey, main() startet den Handler.

Entfernt: weekly_plan_screen.dart, modern_input_field.dart, migration_helper.dart, jeweils ohne Referenz.

Sicherheit

  • Bring!-Secrets im Klartext (#15): Passwort, Access-/Refresh-Token und UUIDs lagen unverschlüsselt in der SQLite-DB, während der eigene Auth-Token längst flutter_secure_storage nutzt. Sie liegen jetzt ebenfalls dort, Altbestand wird beim ersten Lesen migriert und in der DB gelöscht. Gespeichert wird bei onEditingComplete statt bei jedem Tastendruck.
  • Rohe Server-Antworten im UI (#17): ApiException.toString() zeigte den kompletten Response-Body. Jetzt werden Meldungen aus ProblemDetails extrahiert, sonst statuscode-abhängige Texte; der Rohtext geht nur ins Log.
  • Hartkodierte LAN-URL (#16): baseUrl kommt aus --dart-define=API_BASE_URL, jeder Request hat 10 Sekunden Timeout.

Zustand und Fehlerbehandlung

  • Fehler waren unsichtbar (#20): Die Provider loggten nur mit debugPrint, in Release-Builds also gar nicht. RecipeState und MealPlanState haben ein error-Feld, das neue ErrorBanner zeigt es mit Wiederholen-Knopf. Die Notifier werfen bei Schreibfehlern weiter.
  • Wochenstart Sonntag wurde ignoriert (#9): startOfWeek, dayIndexInWeek und weekStartDayName als gemeinsame Helfer in settings_service.dart.
  • "Heute"-Button sprang nicht (#10): Er las den vor dem await erfassten State.
  • Controller-Leaks (#7, #18): Die Controller der Zutaten- und Schrittzeilen wurden pro build neu erzeugt. Sie liegen jetzt im State, werden mit der Zeile entfernt und freigegeben. Login, Register und alle Dialoge räumen ebenfalls auf.

Darstellung

  • Dark Mode (#21) über ColorScheme.fromSeed. MealColors und DifficultyColors als ThemeExtension. Colors.grey/red/blue/green/orange/indigo kommen in lib/ nicht mehr vor.
  • Fokus war unsichtbar (#22): Alle "modern"-Felder hatten BorderSide.none auch für focusedBorder und focusedErrorBorder.
  • Kaputte Umlaute (#13): "Tippe zum VergrБひern" war im UI sichtbar. Alle \uXXXX-Escapes sind durch echte Zeichen ersetzt.
  • Tote Bedienelemente (#26): Der Filter-Button öffnet ein echtes Filter-Sheet, Teilen kopiert das Rezept in die Zwischenablage, der FAB im Tagesplan wählt die Mahlzeit nach Tageszeit, onMealTap springt zur passenden Mahlzeit.
  • Layout (#27): RecipeCard-Chips in einem Wrap, die drei Zahlenfelder stapeln unter 360 dp.
  • Ladezustände (#23): Ladebalken statt Vollbild-Spinner, Pull-to-Refresh in Wochenplan und Rezeptliste.
  • Ansprache durchgehend "du" (#28), Datumsformatierung über intl (#29), Version aus package_info_plus (#31).

Nachweis

vorher jetzt
flutter analyze 79 Meldungen 0
flutter test kein test/ 22 Tests grün

Neu hinzugefügte Dependency: url_launcher für den Update-Link.

Die Tests deckten gezielt die Stellen ab, an denen Fehler gefunden wurden: isoWeekNumber über Jahreswechsel und Sommerzeit, startOfWeek und dayIndexInWeek für beide Wochenstarts, sowie die copyWith-Sentinel-Logik.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB

Behebt die Frontend-Issues #1 bis #29 und #31 aus dem Review. #30 bleibt offen: die Unit-Tests sind da, Widget-Tests und CI fehlen noch. Nur `app/` ist angefasst. Im Working Tree lagen parallel fremde Backend-Änderungen, die bewusst nicht Teil dieses Branches sind. ## Absturz und Datenverlust - **App-Start hing** (#1): Die Versionsprüfung lief vor `runApp()` gegen zwei Requests ohne Timeout. Bei nicht erreichbarem Server blieb die App minutenlang auf dem Splash. Sie läuft jetzt über `appUpdateProvider` nach dem ersten Frame und fängt Fehler selbst ab. - **Rezept speichern stürzte ab** (#4): `int.parse` auf drei Freitextfelder warf bei `1,5` eine `FormatException`. Jetzt Ziffernfilter, numerischer Validator und `tryParse`. - **Rezept speichern verlor Daten** (#5): `addRecipe` wurde nicht awaited, der Screen schloss sofort, Serverfehler gingen als unbehandelte Async-Exception verloren. Jetzt awaited, mit Fehlermeldung, gepoppt nur bei Erfolg. - **Filter ließen sich nicht abwählen** (#6): In `copyWith` hieß `null` "nicht ändern". Neuer `unset`-Sentinel in `lib/util/copy_with.dart`, angewandt auf `RecipeState`, `MealPlanState`, `SpacesState` und `AuthState`. - **Kalenderwoche war falsch** (#8): Die alte Formel lieferte für den 01.01.2026 "KW 0". `isoWeekNumber` rechnet jetzt in UTC über den Donnerstag der Woche. - **`RangeError` bei leerem Rezepttitel** (#24) und **fehlendes `catch`** im Vorschläge-Screen. ## Zwei fertige Features waren unerreichbar - **`SettingsScreen`** (#11) wurde nirgends navigiert. Damit war die gesamte Bring!-Konfiguration nicht bedienbar, obwohl die Detailseite einen "Zu Bring! senden"-Button hat. Jetzt über ein Icon im Wochenplan-Header erreichbar. - **`SharedLinkImportHandler`** (#12) wurde nie instanziiert. Der Router liefert jetzt `rootNavigatorKey`, `main()` startet den Handler. Entfernt: `weekly_plan_screen.dart`, `modern_input_field.dart`, `migration_helper.dart`, jeweils ohne Referenz. ## Sicherheit - **Bring!-Secrets im Klartext** (#15): Passwort, Access-/Refresh-Token und UUIDs lagen unverschlüsselt in der SQLite-DB, während der eigene Auth-Token längst `flutter_secure_storage` nutzt. Sie liegen jetzt ebenfalls dort, Altbestand wird beim ersten Lesen migriert und in der DB gelöscht. Gespeichert wird bei `onEditingComplete` statt bei jedem Tastendruck. - **Rohe Server-Antworten im UI** (#17): `ApiException.toString()` zeigte den kompletten Response-Body. Jetzt werden Meldungen aus ProblemDetails extrahiert, sonst statuscode-abhängige Texte; der Rohtext geht nur ins Log. - **Hartkodierte LAN-URL** (#16): `baseUrl` kommt aus `--dart-define=API_BASE_URL`, jeder Request hat 10 Sekunden Timeout. ## Zustand und Fehlerbehandlung - **Fehler waren unsichtbar** (#20): Die Provider loggten nur mit `debugPrint`, in Release-Builds also gar nicht. `RecipeState` und `MealPlanState` haben ein `error`-Feld, das neue `ErrorBanner` zeigt es mit Wiederholen-Knopf. Die Notifier werfen bei Schreibfehlern weiter. - **Wochenstart Sonntag wurde ignoriert** (#9): `startOfWeek`, `dayIndexInWeek` und `weekStartDayName` als gemeinsame Helfer in `settings_service.dart`. - **"Heute"-Button sprang nicht** (#10): Er las den vor dem `await` erfassten State. - **Controller-Leaks** (#7, #18): Die Controller der Zutaten- und Schrittzeilen wurden pro `build` neu erzeugt. Sie liegen jetzt im State, werden mit der Zeile entfernt und freigegeben. Login, Register und alle Dialoge räumen ebenfalls auf. ## Darstellung - **Dark Mode** (#21) über `ColorScheme.fromSeed`. `MealColors` und `DifficultyColors` als `ThemeExtension`. `Colors.grey/red/blue/green/orange/indigo` kommen in `lib/` nicht mehr vor. - **Fokus war unsichtbar** (#22): Alle "modern"-Felder hatten `BorderSide.none` auch für `focusedBorder` und `focusedErrorBorder`. - **Kaputte Umlaute** (#13): "Tippe zum VergrБひern" war im UI sichtbar. Alle `\uXXXX`-Escapes sind durch echte Zeichen ersetzt. - **Tote Bedienelemente** (#26): Der Filter-Button öffnet ein echtes Filter-Sheet, Teilen kopiert das Rezept in die Zwischenablage, der FAB im Tagesplan wählt die Mahlzeit nach Tageszeit, `onMealTap` springt zur passenden Mahlzeit. - **Layout** (#27): RecipeCard-Chips in einem `Wrap`, die drei Zahlenfelder stapeln unter 360 dp. - **Ladezustände** (#23): Ladebalken statt Vollbild-Spinner, Pull-to-Refresh in Wochenplan und Rezeptliste. - Ansprache durchgehend "du" (#28), Datumsformatierung über `intl` (#29), Version aus `package_info_plus` (#31). ## Nachweis | | vorher | jetzt | |---|---|---| | `flutter analyze` | 79 Meldungen | 0 | | `flutter test` | kein `test/` | 22 Tests grün | Neu hinzugefügte Dependency: `url_launcher` für den Update-Link. Die Tests deckten gezielt die Stellen ab, an denen Fehler gefunden wurden: `isoWeekNumber` über Jahreswechsel und Sommerzeit, `startOfWeek` und `dayIndexInWeek` für beide Wochenstarts, sowie die `copyWith`-Sentinel-Logik. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Behebt #1, #2, #3, #16, #17.

- main(): Versionspruefung blockiert den Start nicht mehr. Sie laeuft ueber
  appUpdateProvider nach dem ersten Frame und faengt Fehler selbst ab.
- RecipePlanerApp: needsUpdate/updateUri waren untypisiert, keinem Feld
  zugewiesen und ungenutzt. Ersetzt durch ein Banner, das den Update-Link
  per url_launcher oeffnet.
- AppInfoService: liest jetzt camelCase (latestVersion/apkUrl) wie vom
  Backend geliefert, statt PascalCase.
- ApiClient: baseUrl ueber --dart-define=API_BASE_URL konfigurierbar,
  10s Timeout auf jedem Request, Socket-/Timeout-Fehler als lesbare
  Meldung. Rohe Response-Bodies gehen nur noch ins Log, das UI bekommt
  aus ProblemDetails extrahierte oder statuscode-abhaengige Texte.
- AppTheme: darkTheme ergaenzt, ColorScheme.fromSeed, Fokus- und
  Fehlerrahmen im inputDecorationTheme, MealColors/DifficultyColors als
  ThemeExtension statt harter Colors.indigo & Co.
- intl: initializeDateFormatting('de_DE') und Cupertino-Delegate ergaenzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Behebt #6, #8, #10, #20, #22.

- copyWith: neuer unset-Sentinel in lib/util/copy_with.dart. Bisher hiess
  `null` "nicht aendern", dadurch liessen sich Filter nicht zuruecksetzen
  und currentSpace/currentWeekPlan nicht leeren. Betrifft RecipeState,
  MealPlanState, SpacesState und AuthState.
- error-Feld in RecipeState und MealPlanState, hasLoadedOnce in
  SpacesState. Fehler landen nicht mehr nur im debugPrint, sondern als
  ErrorBanner mit "Erneut" im UI.
- addMealToPlan/removeMealFromPlan/addCustomMealToPlan werfen jetzt weiter,
  damit Aufrufer den Fehlschlag nicht als Erfolg melden.
- settings_service: startOfWeek, dayIndexInWeek und isoWeekNumber als
  gemeinsame Helfer. isoWeekNumber rechnet in UTC und ueber den Donnerstag
  der Woche, die alte Formel lieferte fuer den 01.01.2026 "KW 0".
- ModernWeekField/ModernDateField sind StatefulWidgets, der Controller wird
  nicht mehr pro build erzeugt und wird disposed.
- Fokus- und Fehlerrahmen in allen modern-Feldern wieder sichtbar,
  ModernIconButton ohne leeren Tooltip, Suchfeld mit Controller-Listener.
- DailyPlanScreen: Wochenindex folgt dem eingestellten Wochenstart, der
  Heute-Button liest den State nach dem await frisch, Bottom Sheet wird vor
  dem Dialog geschlossen, Navigator wird vor pop() gegriffen, Dialog-
  Controller wird freigegeben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Behebt #4, #5, #14, #25.

- Zahlenfelder: FilteringTextInputFormatter.digitsOnly plus numerischer
  Validator, Speichern nutzt int.tryParse. Bisher warf int.parse bei
  Eingaben wie "1,5" eine FormatException und der Button reagierte nicht.
- _saveRecipe ist async und awaited add/updateRecipe. Vorher schloss sich
  der Screen sofort und ein Serverfehler ging als unbehandelte
  Async-Exception verloren, obwohl der Notifier rethrow macht.
- Leere Zutaten und Schritte werden vor dem Speichern verworfen. Die
  Pflichtpruefung war wirkungslos, weil _ensureMinimumFields immer eine
  leere Zutat anlegt.
- Controller der Zutaten- und Schrittzeilen liegen im State statt in build,
  werden mit der Zeile entfernt und in dispose freigegeben. Listenelemente
  haben jetzt ValueKeys.
- Reorder-Dialog nutzt StatefulBuilder, dadurch bleibt die neue Reihenfolge
  sichtbar. asMap statt indexOf, das bei doppelten Pfaden falsch lag.
- Tag-Feld hat einen Controller und leert sich nach Enter.
- PopScope fragt vor dem Verwerfen ungespeicherter Aenderungen nach.
- return im finally entfernt, es verschluckte Exceptions aus dem catch.
- Import-Dialog gibt seinen Controller frei.
- Speichern-Button zeigt einen Spinner in Button-Groesse und ist waehrend
  des Speicherns gesperrt.
- Zahlenfelder brechen unter 360dp untereinander um, statt die Labels
  abzuschneiden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Behebt #13, #24, teilweise #18, #20, #23, #26, #27.

- recipe_detail_screen: kaputtes Encoding korrigiert ("Tippe zum
  Vergroessern" war im UI sichtbar). Teilen kopiert das Rezept jetzt als
  Text in die Zwischenablage statt "wird implementiert" zu melden.
  Image.file bekommt errorBuilder, der PageController des Bilddialogs wird
  freigegeben und der Titel folgt der angezeigten Seite.
- recipes_screen: Filter-Button oeffnet ein echtes Filter-Sheet, der tote
  onPressed und der Platzhalter-Dialog sind weg. Leerzustand unterscheidet
  "keine Rezepte" von "Filter ohne Treffer". Loeschen wird awaited und
  meldet Fehler. Pull-to-Refresh, Fehlerbanner, Ladebalken statt
  Vollbild-Spinner.
- recipe_suggestion_screen: catch ergaenzt, mounted-Pruefung vor setState,
  recipe.title[0] warf bei leerem Titel RangeError. Die Filter
  maxPreparationTime und maxDifficulty haben jetzt eine Oberflaeche.
  addMealToPlan wird awaited, statt Erfolg zu melden und zu schliessen.
- space_selection_screen: Ladefehler wird angezeigt statt als "Keine Spaces
  gefunden" getarnt, mit Abmelden-Ausweg aus der Router-Weiterleitung.
  createSpace meldet Fehlschlaege, Controller werden freigegeben,
  Navigator/Messenger vor dem await gegriffen. _SpaceCard nutzt
  onPrimaryContainer statt onPrimary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Behebt #7, #11, #12, #15, #18, #19, #21, #22, #23, #26, #27, #28, #29, #30, #31.

Erreichbarkeit und toter Code:
- SettingsScreen ist ueber ein Icon im Wochenplan-Header erreichbar. Damit
  sind Wochenstart und Bring!-Konfiguration erstmals bedienbar.
- SharedLinkImportHandler wird in main() gestartet, der Router liefert
  dafuer einen navigatorKey. Der Share-Import war bisher tot.
- Entfernt: weekly_plan_screen.dart (Duplikat von Home/DailyPlan),
  modern_input_field.dart und migration_helper.dart, jeweils 0 Referenzen.

Sicherheit:
- Bring!-Passwort, Access-/Refresh-Token und die UUIDs liegen jetzt in
  flutter_secure_storage statt im Klartext in der SQLite-DB. Altbestand
  wird beim ersten Lesen migriert und in der DB geloescht.
- Die Felder speichern bei onEditingComplete statt bei jedem Tastendruck.

Analyzer: von 79 auf 0 Meldungen.
- withOpacity -> withValues, surfaceVariant -> surfaceContainerHighest,
  RadioListTile -> RadioGroup, DropdownButtonFormField.value ->
  initialValue, ungenutzte Imports und Elemente entfernt,
  use_build_context_synchronously bereinigt, \uXXXX-Escapes durch echte
  Umlaute ersetzt.

Darstellung:
- Harte Farben (Colors.grey/red/blue/green/orange/indigo) durch
  ColorScheme-Tokens und die neuen MealColors/DifficultyColors ersetzt.
- MealPlanDayCard nutzt Material+Ink, der Ripple war vorher verdeckt.
- RecipeCard-Chips in einem Wrap statt starrer Row.
- Ladebalken statt Vollbild-Spinner, Pull-to-Refresh in Wochenplan und
  Rezeptliste.
- Wochentage und Datumsangaben kommen aus intl, die drei handgepflegten
  Wochentags-Arrays sind weg.
- Version in den Einstellungen kommt aus package_info_plus.
- Ansprache durchgehend auf "du", Login/Register mit E-Mail-Pruefung,
  Passwort-Mindestlaenge, autofillHints und Spinner in Buttongroesse.
- Login-, Register- und Dialog-Controller werden freigegeben.

Tests: erstes test/-Verzeichnis mit 22 Tests fuer isoWeekNumber,
startOfWeek, dayIndexInWeek und die copyWith-Sentinel-Logik.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
- SettingsScreen: Ladebalken in der AppBar statt Vollbild-Spinner, die
  Seite bleibt beim Laden sichtbar. Rest von #23.
- Generierte Plugin-Registranten fuer Linux, macOS und Windows nach dem
  Hinzufuegen von url_launcher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTprHbQnuPxu5GeStnCnB
Aceless force-pushed frontend-improvements from 47edeeb9d8 to f628f68a5a 2026-09-09 21:27:53 +00:00 Compare
Author
Owner

Hinweis zur Merge-Reihenfolge

Der Branch backend/issue-fixes wurde von frontend-improvements abgezweigt und enthaelt deshalb die sieben Frontend-Commits dieses PRs mit.

Dieser Branch wurde danach umgeschrieben und force-gepusht: ein Commit hatte drei Backend-Dateien mitgeloescht, die nicht in diesen PR gehoeren (backend/.env und beide appsettings*.json). Sie sind wieder enthalten, der PR umfasst jetzt ausschliesslich app/. Die Commit-Hashes haben sich dadurch geaendert.

Fuer den Merge bedeutet das: backend/issue-fixes traegt noch die alten Hashes der Frontend-Commits. Werden beide Branches gemergt, landen diese Aenderungen inhaltlich doppelt in der Historie. Sauberer ist, backend/issue-fixes vor dem Merge auf main zu rebasen, damit dort nur die Backend-Commits liegen.

Unabhaengig von diesem PR: der OpenAI-Key in backend/.env, das JWT-Secret und das DB-Passwort in appsettings.Development.json liegen in der Historie von main. Sie muessen rotiert werden, ein spaeteres Loeschen der Dateien genuegt nicht. Siehe #32 und #35.

## Hinweis zur Merge-Reihenfolge Der Branch `backend/issue-fixes` wurde von `frontend-improvements` abgezweigt und enthaelt deshalb die sieben Frontend-Commits dieses PRs mit. Dieser Branch wurde danach umgeschrieben und force-gepusht: ein Commit hatte drei Backend-Dateien mitgeloescht, die nicht in diesen PR gehoeren (`backend/.env` und beide `appsettings*.json`). Sie sind wieder enthalten, der PR umfasst jetzt ausschliesslich `app/`. Die Commit-Hashes haben sich dadurch geaendert. Fuer den Merge bedeutet das: `backend/issue-fixes` traegt noch die alten Hashes der Frontend-Commits. Werden beide Branches gemergt, landen diese Aenderungen inhaltlich doppelt in der Historie. Sauberer ist, `backend/issue-fixes` vor dem Merge auf `main` zu rebasen, damit dort nur die Backend-Commits liegen. Unabhaengig von diesem PR: der OpenAI-Key in `backend/.env`, das JWT-Secret und das DB-Passwort in `appsettings.Development.json` liegen in der Historie von `main`. Sie muessen rotiert werden, ein spaeteres Loeschen der Dateien genuegt nicht. Siehe #32 und #35.
Sign in to join this conversation.
No description provided.