- record_walk gibt total_km zurück → Toast "🐾 X km gezählt · Lebenswerk Y km".
- Nachtrag-Toast (_flushPendingNavWalk) zeigt ebenfalls km + Lebenswerk.
- list_routes liefert my_walk_count + my_last_walked → Routen-Karte zeigt
"🐾 X× gelaufen · zuletzt heute/gestern/vor N Tagen".
Macht für Angie sichtbar, dass das Ablaufen einer gespeicherten Route mitzählt.
- /osm-auth/status liefert signup_url + sandbox-Flag (Sandbox-URL auf Staging,
echte OSM in Prod).
- Settings-OSM-Karte: ausklappbare Hilfe "Noch kein OSM-Konto? Was ist das?"
mit Erklärung, 3-Schritt-Anleitung, Sandbox-Testphasen-Hinweis und
"Kostenloses OSM-Konto erstellen"-Link zur richtigen Instanz.
- dog=no zusätzlich zu dog=yes (Pächterwechsel → Ort nicht mehr hundefreundlich).
- Map-Popup: ein "Hund willkommen?"-Block mit Daumen hoch/runter statt zwei
Buttons. Beide rufen /dog-friendly mit welcome=true|false.
- Backend generisch: tag_value yes|no; vorhandene Markierung mit anderem Wert
wird umgedreht (Update statt 409); submit_dog_tag(value); Confirm/Revert prüft
gegen den jeweiligen tag_value; Changeset-Kommentar wertabhängig.
Bug: daily_photo_cache zeigte auf gelöschte Tagebuch-Foto-URLs, weil
Löschen eines Eintrags oder einzelnen Medien-Items den Cache nicht
mit-bereinigte. Heim-Tab in der iOS-App lud dann 404 → kein Tagesbild.
Fix in dogs.py /welcome-dashboard:
- Bevor das Cache-Foto zurückgegeben wird, prüfen ob die URL noch in
diary_media existiert. Wenn nicht: Cache-Eintrag löschen und neu
wählen → selbstheilend für alte verwaiste Einträge.
Fix in diary.py:
- delete_diary: vor dem CASCADE-Delete von diary_media die URLs
sammeln und alle daily_photo_cache-Zeilen darauf löschen.
- delete_media_item: gleicher Cleanup für die eine URL.
Cache ist klein (max 1 Eintrag pro Hund pro Tag) — Hygiene-Cleanup
ist günstig und macht das System defensiv.
- settings.js Header-Badge unter dem Namen leitet jetzt aus
subscription_tier ab (analog _tierCard / has_pro_access): Admin/
Moderator, Züchter, Pro, sonst 'Kostenlos'. Vorher las nur das alte
is_premium-Flag, was beim Admin-Upgrade nicht mitgezogen wurde.
- admin.py fulfill_upgrade_request setzt jetzt is_premium synchron mit
subscription_tier (1 für pro/breeder, sonst 0). Hält Login-Response,
/auth/me und Reports konsistent.
- admin.py delete_user: löscht jetzt auch Hund-zentrierte Daten (diary,
health, training_sessions, training_streaks, expenses), dogs,
upgrade_requests, push_subscriptions, notifications, forum_posts
bevor der User-Row weg ist. Vorher: nur DELETE FROM users → Waisen in
allen FK-Tabellen.
- profile.py delete_account: gleicher Cleanup-Set, vergisst jetzt
upgrade_requests nicht mehr.
- admin.py Dashboard-Counter 'Zu Erledigen': JOIN users, damit
verwaiste Anfragen nicht mehr im Header-Badge erscheinen (Liste
selbst filtert sie schon korrekt via JOIN). Bump 1135→1136.
Neue Tabelle daily_photo_cache(dog_id, datum, photo_url) hält die Wahl
fest, sobald irgendein Client (PWA oder iOS) das erste Mal heute fragt.
Alle nachfolgenden Aufrufe liefern dieselbe URL — kein Kippen mehr, wenn
zwischendrin neue Fotos hochgeladen werden und die tick%len-Rotation auf
einen anderen Index zeigt. CREATE TABLE IF NOT EXISTS inline, daher kein
Migrations-Eingriff nötig.
KATEGORIE_META als Dict mit id → {label, color} (preserved Order).
KATEGORIEN bleibt als Set für Validierung. Neuer Endpunkt liefert
[{id, label, color}, …] als Single Source of Truth für PWA und mobile
Clients — bisher war die Liste in JS und Python dupliziert.
Mobile-Client (banyaro-ios) holt die Liste jetzt dynamisch.
Aus Container-Log gefundene Backend-Errors:
1. _job_anniversary_reminders: 'no such column: d.user_id'
diary-Tabelle hat keine user_id — User-Bezug geht über dogs.user_id.
→ JOIN dogs ON dogs.id = d.dog_id ergänzt + SELECT dogs.user_id.
Job läuft täglich 09:00 — war seit Tag X kaputt, kein Push für
Jahrestage gesendet.
2. RASFF API 404 (EU Rapid Alert System for Food and Feed):
webgate.ec.europa.eu/rasff-window/backend/public/... ist umgezogen.
→ HTTPStatusError mit 404/410/503 wird jetzt nur als WARNING geloggt
(vorher ERROR → Error-Digest spammte täglich). Fallback ist eh schon
ein leeres Array, App läuft weiter. EU-Endpoint-URL muss nochmal
recherchiert werden, dann RASFF_URL aktualisieren — Folge-Sprint.
NEUE HELPER in auth.py:
require_moderator(user=Depends(get_current_user))
Konsequente Dependency statt inline
'if user["rolle"] not in ("admin", "moderator")'
require_breeder(user=Depends(get_current_user))
Konsequente Dependency statt inline
'if user["subscription_tier"] not in ("breeder", "breeder_test")'
require_owner(row, user, owner_field='user_id',
not_found_msg, forbidden_msg) -> row
Zentralisiert das häufigste Pattern (54 Stellen im Audit):
Statt:
row = conn.execute(...).fetchone()
if not row: raise HTTPException(404, ...)
if row['user_id'] != user['id']: raise HTTPException(403, ...)
Jetzt:
row = require_owner(conn.execute(...).fetchone(), user,
not_found_msg='Ort nicht gefunden.')
is_owner_or_admin(row, user, owner_field='user_id') -> bool
True wenn Owner ODER Admin/Moderator (Admin-Override für
Moderations-Endpoints)
DEMO-MIGRATION:
places.py PATCH /places/{id} + DELETE /places/{id} migriert auf
require_owner() — als Style-Referenz für künftige Migrationen.
KEINE Massen-Migration der 54 Stellen — bewusste Entscheidung
weil security-kritisch. Helper sind bereitgestellt, neuer Code
nutzt sie, bestehender bleibt funktional identisch.
Tests 19/19 grün.
Hinweis: Massen-Migration der Owner-Checks ist eigener Sprint mit
sehr sorgfältigem Testing — bei jeder migrierten Route muss die
404→403→Cascade durchgeprüft werden, dass Owner+Non-Owner+Admin
sich identisch zum Vorher verhalten.
A — Founder-Number-Race (Audit-Fund aus Agent 2)
- partner.py PATCH /admin/users: SELECT COUNT + UPDATE+1 →
atomares UPDATE mit Sub-Query. WHERE-Klausel prüft Limit + dass
User noch nicht is_founder=1 ist. rowcount=0 → 'Plätze vergeben'.
- dogs.py POST /dogs (erster Hund triggert Gründer-Aktivierung): selbes
Pattern. Zusätzlich AND is_founder_pending=1 als Schutz.
- Sub-Queries werden gegen Snapshot VOR dem UPDATE evaluiert
(SQL-Spec), daher keine 'doppelte Nummer' möglich auch wenn zwei
User gleichzeitig den ersten Hund anlegen.
B — JWT-Blacklist-Cleanup-Job
- _purge_expired_jwt() in auth.py existierte schon, war aber nicht
verdrahtet → jwt_blacklist wuchs monoton.
- Neuer Scheduler-Job _job_purge_jwt_blacklist täglich 03:30
(nach poison_archive, in ruhiger Zeit), mit _log_job für
Error-Digest.
C — iOS Storage-Quota-Watchdog (PWA-Stabilität)
- offline-indicator.js: _checkStorageQuota() per
navigator.storage.estimate() beim Init + alle 60s im Interval.
- Bei >=80% Auslastung: Tile-Cache auf 100 Einträge trimmen (statt
default 500). Verhindert QuotaExceededError auf iOS-PWA (~50MB).
- Bei >=90%: einmaliger Toast-Hinweis pro Session
'Speicher fast voll — Tiles werden gelöscht'.
D — HTTPException in osm.py
- 'raise Exception("Alle Overpass-Instanzen fehlgeschlagen")' wurde
zu HTTP 500 → User-unfriendly. Jetzt 503 mit klarer Message
'Kartendaten gerade nicht verfügbar'.
Tests 19/19 grün.
- /admin/stats liefert jetzt zusätzlich user_poi_total + user_poi_by_type
(User-POIs aus user_map_pois aufgeschlüsselt, komma-separierte Typen
werden einzeln gezählt)
- Admin System-Tab zeigt zwei Karten:
· OSM-Cache nach Typ (was Overpass-Cache enthält)
· Nutzer-POIs nach Typ (selbst erstellte Marker)
- Interne Typ-Namen werden in lokalisierte Labels gemappt
(tierarzt → Tierarzt, hundesalon → Hundesalon, etc.)
- Header der Karten zeigt Gesamtzahl inline
- OSM-Query 'hundesalon' nutzt shop=pet_grooming + craft=pet_grooming
- map.js: neuer Layer 'hundesalon' mit Schere-Icon (#EC4899 Pink),
in Layer-Liste, TYPEN, OSM_LAYER_MAP und PIN_TYPES eingetragen
- User können selbst Hundesalons als POI anlegen
(places.py TYPEN + osm.py ALLOWED_TYPES erweitert)
- Backend: /admin/upgrade-requests liefert pro Request die offene
Rechnung (id+number+status) per Subquery aus der invoices-Tabelle
(status draft|sent → also nicht bezahlt, nicht storniert)
- Frontend: Wenn schon eine Rechnung existiert, wird statt 'Rechnung
erstellen' (orange) der Button 'Rechnung bearbeiten' (gelb,
#eab308) gezeigt. Klick lädt die Rechnung und öffnet das Modal im
Edit-Modus — kein doppeltes Anlegen, Nummerierung bleibt sauber.
- Verlauf: _verlaufLoading-Flag verhindert parallele Loads
- Verlauf: el nach await neu holen (stale DOM-Referenz nach Re-Render)
- Verlauf: bei _renderContent() Shell nur rendern wenn keine Sessions im Cache
- Backend: hund_stimmung/zufriedenheit als Optional[str/int] → akzeptiert null
- Neuer Tab 'Protokoll' in der Übungen-Seite: zeigt alle Trainingseinheiten
chronologisch nach Datum gruppiert (Heute/Gestern/Datum-Label)
- Jede Einheit: Übungsname, Wdh., Erfolgs-Emoji, Stimmung, Sterne, Notiz, TOP-Badge
- 'Weitere laden' Pagination (30 Einheiten pro Seite)
- Backend: Training erstellt keine Tagebuch-Einträge mehr (weder bei ist_top noch manuell)
- Frontend: 'Als Meilenstein ins Tagebuch' Checkbox komplett entfernt
- onDogChange setzt Verlauf-State zurück
Alle drei Rechnungs-Einstiegspunkte (Admin-Upgrade-Button, automatische
Verlängerung via Scheduler, manuelles Upgrade via admin.py) erhalten jetzt
den einheitlichen Hinweis zum Jahresbeitrag gem. AGB ohne Rückerstattung.