App-Review-Fix (Guideline 2.1 WeatherKit):
- OneShotLocation: deterministisches async resolve() mit 10s-Timeout statt
onChange-Lauschen; WetterView lädt bei fehlendem Standort einen Berlin-Fallback
→ kein ewiges Hängen bei "Hole Standort…", WeatherKit ist immer sichtbar.
Offline-Lesen (SwiftData):
- CachedRoute/CachedDiaryEntry/CachedImage + CachedAsyncImage: Touren, Tagebuch
und Fotos werden cache-first geladen und sind offline verfügbar.
- Cache wird bei Logout/401 geleert (RootView), kein Durchschimmern fremder User.
Offline-Speichern (Outbox):
- PendingRoute/PendingRoutePhoto: Tour inkl. unterwegs hinzugefügter Fotos wird
offline lokal gesichert und automatisch hochgeladen (Touren-Tab + App-Start).
- Touren-Liste zeigt offline gesicherte Touren mit "wird hochgeladen"-Badge.
FinishWalkSheet:
- Dismiss-Schutz: Speichern-Dialog lässt sich nicht mehr wegwischen — eine
aufgezeichnete Tour geht nicht mehr durch Runterwischen verloren.
Wetter:
- Ortslabel (Reverse-Geocoding; Fallback "Berlin · Näherung").
- Saubere Offline-Meldung statt rohem networkError.
Aufräumen:
- Doppeltes "Gassi-Treffen" im Mehr-Tab entfernt.
- Veraltete Phase-1/2-Texte neu getextet.
- Tote DogsListView gelöscht (Hund-Wechsel läuft über den Heim-Picker).
Detail-Karte zeigt jetzt Treffpunkt + User-Position via UserAnnotation,
darunter eine Navigation-Karte mit:
- Ortsname
- Luftlinien-Distanz vom aktuellen Standort
- 'Route'-Button öffnet Apple Maps (MKMapItem.openInMaps) — User wählt
dort Walking/Driving
Plus Teilnehmer-Sektion mit GET /api/walks/{id}/participants:
- Liste der Zugesagten ('yes'-RSVP) mit Namen + Hunden
- 'Noch niemand zugesagt'-Hinweis bei leerer Liste
- myRsvp-Status für korrekte Join-Button-Anzeige
- isOwn → 'Dein Treffen'-Badge statt Beitreten-Button
Footer-Hinweis: 'Fotos vom Treffen kannst du später in der banyaro.app
teilen' — Foto-Funktion bewusst PWA-only (User-Wunsch).
In der PWA ist die Seite 'Gassi-Treffen' mit drei Tabs:
- Treffen (walks.py — sich verabreden)
- Challenge (Monatsfoto)
- Stamm-Gassis (gassi_zeiten.py — regelmäßige Runden)
Mein bisheriger Mehr-Eintrag hieß 'Stamm-Gassi-Zeiten' und zeigte nur die
Stamm-Gassi-Funktion isoliert — das stimmte nicht mit der PWA überein.
Neu:
- GassiView mit Segmented Picker (Treffen / Stamm-Gassis)
- GassiTreffenList: GET /api/walks?lat&lon&radius=20000, Liste mit Datum,
Uhrzeit, Ort, Teilnehmer-Zahl
- GassiTreffenDetail: Karte mit Pin, Stats, Beitreten/Verlassen
(POST/DELETE /api/walks/{id}/join), Owner-Check
- AddWalkSheet: Titel, Datum, Uhrzeit, Treffpunkt-Name, Max-Teilnehmer,
Beschreibung — POST /api/walks
- StammGassisList = bisherige GassiZeitenView umbenannt + Nav-Title raus
(wird vom GassiView vergeben)
Im Mehr-Tab heißt der Link jetzt 'Gassi-Treffen' (pawprint-Icon) statt
'Stamm-Gassi-Zeiten' (alarm-Icon).
DTOs: WalkMeeting, WalkCreateBody, WalkJoinBody.
URL.appending(path:) behandelt den Input als reinen Path-Component und
percent-encoded Sonderzeichen, also auch ?. Damit wurde aus
/api/dogs/123/diary?limit=50 ein /api/dogs/123/diary%3Flimit=50, und der
Server lieferte was Anderes als JSON zurück → 'data was not valid JSON'.
Betraf auch Wetter, Giftköder, Verlorene Hunde, Gassi-Zeiten und alle
anderen Endpoints mit Query. Jetzt: baseURL.absoluteString + path.
Backend liefert das Foto-Array unter 'media_items' (in _entry_dict),
nicht 'media'. Außerdem ist is_milestone wieder eine SQLite-Int-Spalte
0/1 — der bekannte Bool-Quirk. Mit Bool? in der DTO bricht die Decoder
ab und die ganze Liste verschwindet stillschweigend in den Fehler-Zustand.
ExpenseCategory DTO + GET /api/expenses/categories beim Öffnen der Liste
sowie bei Refresh. Falls der Endpunkt noch nicht ausgerollt ist (oder
fehlschlägt), Fallback auf eine lokale Default-Liste mit den aktuellen
sechs Kategorien.
AddExpenseSheet bekommt die Kategorien als Parameter, statt eigene
Liste zu führen — Source of Truth ist jetzt das Backend.
Tagebuch (Diary):
- DiaryEntry + DiaryMedia + DiaryCreateBody DTOs
- TagebuchView: Liste der Einträge für aktiven Hund mit Titel, Text,
Ortsname, Meilenstein-Stern, Foto-Strip
- AddDiaryEntrySheet: Titel/Text/Datum/Meilenstein/Ort/Tags +
PhotosPicker, nach POST /api/dogs/{id}/diary werden Fotos einzeln
via POST /api/dogs/{id}/diary/{entry_id}/media hochgeladen (mit
ImageResize.resizedJPEG)
Heim-Tab als neuer 1. Tab:
- DashboardSnapshot DTO für /api/dogs/{id}/welcome-dashboard
- ActiveDogStore (@Observable + UserDefaults("activeDogId")): hält
den aktiven Hund app-weit
- HeimView: tägliches Hintergrundfoto aus random_photo.url (rotiert
pro Tag, vom Backend gewählt), Gradient zur Lesbarkeit, Tagezeit-
Begrüßung mit User-Namen, Hund-Picker (Menu), Info-Karten für
letzten Eintrag/nächsten Termin/Gewicht/Eintragszahl,
Quick-Action-Buttons (Tagebuch, Wetter, Erste Hilfe)
Reorganisation:
- 5 Tabs: Heim, Touren, Aufnehmen, Statistik, Mehr
- Hunde-Liste wandert in Mehr → "Hund & Alltag"
- Tagebuch in Mehr → "Hund & Alltag" + erreichbar von Heim
Pitch-Karte erweitert um die neuen Features (sowie Hundesitting, Züchter).
Neue DTOs in DTOs.swift:
- Expense + ExpenseCreateBody
- GassiZeit + GassiZeitCreateBody (mit wochentage [String], radius_m)
- PoisonAlert + PoisonCreateBody
- LostDog + LostDogCreateBody
- WeatherForecast + WeatherDay (mit asphalt_temp, zecken, pollen-Felder)
Neue Views:
- ErsteHilfeView + Detail: sechs Notfall-Topics (Vergiftung, Hitzschlag,
Wunden, Atemnot, Krampfanfall, Magendrehung) — komplett offline, kein API
- AusgabenView: Liste mit Total, AddExpenseSheet mit Kategorie/Betrag/
Datum/Hund-Picker
- WetterView: One-Shot Location + /api/weather/forecast, 7-Tage-Vorhersage
mit Hunde-Tipps (Hitze ab 25°/30°, Frost, Asphalt ≥50°, Zecken, Regen)
- GassiZeitenView: eigene Zeiten + Add-Sheet (Wochentag-Picker, Hund-
Auswahl), automatische lokale UNCalendarNotifications via Scheduler
- GiftkoederView: Map mit Pins + Liste in 5km Umkreis, Report-Sheet mit
Typ-Auswahl
- VerloreneHundeView: Liste mit Foto/Distanz, Detail mit Karte
Support:
- OneShotLocation: kleiner CLLocationManager-Wrapper für einmalige
Positionsabfrage (Wetter, Giftköder)
- GassiZeitenScheduler: UNCalendarNotificationTrigger pro Wochentag,
Identifier-Schema "gz-{id}-{weekday}"
Navigation: Section "Hund & Alltag" im Mehr-Tab mit NavigationLinks zu
allen sechs neuen Ansichten.
D.10 401-Handling: APIError.unauthorized, NotificationCenter-Bridge,
AuthSession.logout() bei 401 → User landet wieder im Login
D.12 PWA-Deep-Links: Settings-Section mit Forum/Hunde/Walks/Settings
öffnet Safari per https://banyaro.app/#fragment
B.4 Auto-Pause: 2-min-Inaktivität → isAutoPaused, automatischer Resume bei
nächstem GPS-Update. Settings-Toggle, im UI eigenes Badge "Auto-Pause"
(grau vs. Pause orange).
C.7 Edit/Delete: RouteUpdateBody + APIClient.patch + APIClient.delete,
EditRouteSheet (Name/Beschreibung/Public), Menu in Toolbar (nur eigene
Touren), Alert für Delete.
C.9 Statistik-Tab: neuer Tab "Statistik" zwischen Hunde und Mehr. Filtert
/api/routes auf meine Touren, rechnet Woche/Monat/Allzeit (Distanz, Dauer,
Touren), Längste Tour, aktuelle Streak (Tage in Folge).
B.5 Walk-Review: Map-Header an die Spitze des FinishWalkSheet-Forms.
B.6 Geo-Fotos: CapturedPhoto (Data + GPSPoint?), PhotoLocation @Model in
SwiftData. Kamera während Walk taggt mit tracker.points.last. Nach Upload:
foto_url aus Response → PhotoLocation persistiert. MiniRouteMap rendert
Annotations mit Tap-Callback, PhotoViewerSheet zeigt Foto fullscreen.
C.8 Share PNG+GPX: RouteShareImage (MKMapSnapshotter + Polyline overlay +
SwiftUI ShareCard via ImageRenderer), GPXExporter (Tempfile mit XML),
ShareSheet (UIActivityViewController-Wrapper), Menu in Route-Toolbar.
D.11 Icon-Varianten: AppIcon-Dark (0.45 Brightness), AppIcon-Tinted
(Grayscale + Kontrastverstärkung), Contents.json mit appearance entries.
A.2 HealthKit: BanYaroGo.entitlements (com.apple.developer.healthkit),
NSHealthShare/UpdateUsageDescription. WalkHealthSync.shared mit
HKWorkoutBuilder (.walking) + HKWorkoutRouteBuilder, Timestamps gleichmäßig
über Walk-Dauer verteilt. Settings-Toggle mit Permission-Request.
- APIClient.uploadFile: multipart POST mit Bearer-Token, generischer
field/filename/mime
- ImageResize: längste Kante max 2048px, JPEG q=0.8 — iPhone-Fotos sonst
5-10MB pro Stück
- FinishWalkSheet:
- PhotosPicker (iOS 16+, kein NSPhotoLibraryUsageDescription nötig)
- Thumbnail-Strip der gewählten Fotos
- Sequentieller Upload nach POST /api/routes, Toolbar zeigt "N/M"
- Bei < 50m: orangene Warnung "Sehr kurze Tour — du kannst trotzdem speichern"
- Save-Button blockt korrekt während Upload, Verwerfen auch
Das Backend bool-konvertiert nur is_premium explizit; alle anderen 0/1-Spalten
gehen unverändert durch FastAPI. Decode-Fehler vorher still verschluckt → jetzt
auch geloggt, damit das nicht nochmal passiert.
Login liefert nur {token, name, is_premium}. Für Admin-/Founder-/Tier-Info
holen wir nach Login (und beim Erscheinen von MainTabView) /api/auth/me und
zeigen ein echtes Profil mit Avatar, Email, Rolle und nur dann Premium-Status,
wenn das relevant ist.