Praxisfall: Antwort wird serverseitig erstellt, aber die HTTP-Antwort geht
unterwegs verloren (schlechtes Netz). UI zeigt Fehler statt Erfolg, Text bleibt
stehen -> Nutzer tippt erneut -> 2. Versuch laeuft in den 30s-Cooldown (429),
der bereits gepostete Beitrag bleibt unsichtbar.
- forum_posts.client_uuid (Migration). Reply mit stabiler client_uuid:
Retry liefert den BEREITS erstellten Post zurueck (kein Cooldown/Doppelpost).
- Frontend: UUID bleibt ueber Retries stabil, Reset erst nach Erfolg; Foto-
Doppel-Upload bei Retry verhindert.
- Anti-Spam-Cooldown bleibt fuer echte neue Posts aktiv.
- Tests: tests/test_forum_idempotency.py (Retry=selber Post, Cooldown greift,
ohne UUID rueckwaertskompatibel).
Bisher holte nur der Create-/Foto-EXIF-Pfad Wetter+POIs. Wer einem Eintrag
nachträglich (Edit) einen Standort gab, bekam nichts. Jetzt: GPS neu/geändert
im Update-Handler -> POIs immer + Wetter fürs Eintragsdatum.
- weather.get_weather_for_date(): heute -> aktuelles Wetter; Vergangenheit ->
stündliche Historie (Open-Meteo Forecast <=90 Tage, sonst Archive-API),
Stunde aus created_at. Gleiche Dict-Struktur wie get_weather_for_location.
- diary.update_diary(): erfasst alten GPS-Stand, reichert nach DB-Commit an
(async), schreibt nur erfolgreich geholte Felder (kein Datenverlust bei
API-Fehler), identische Koordinaten -> kein erneuter Abruf.
- Tests: tests/test_diary_location_enrich.py (Anreicherung, kein GPS=kein
Abruf, Resave ohne Re-Fetch).
- Anpinnen-Scope: pin_scope ('global' | 'kategorie'). Global haelt oben in
jeder Ansicht; Themen-Pin nur in der gefilterten Kategorie (nicht in 'Alle').
- Bugfix Berechtigung: Forum pruefte nur is_moderator -> Admins ohne das Flag
wurden ausgesperrt. Neuer Helper _can_moderate() = rolle in (admin,moderator)
ODER is_moderator, an allen 7 Forum-Checks + beiden Frontend-isMod-Gates.
- Thread-Detail-Toolbar (nur Admin/Mod): 'Global anpinnen' / 'Im Thema anpinnen'
/ 'Loesen' + Status- und Badge-Anzeige nach Scope.
- DB-Migration forum_threads.pin_scope (idempotent, Default 'global').
- Tests: tests/test_forum_pinning.py (Berechtigung + Scope-Sortierung).
Eigener Song (KI-Demo via Suno) als Marken-Hymne. Dezente Player-Karte unter
dem Tageszitat; preload=none → 6 MB MP3 lädt erst bei Play, der SW cacht sie
danach für offline. Der Banner ist einmalige Einladung und verschwindet nach
erstem Hören (durchgehört oder >30s + Pause); danach dezenter runder Play-Button
unten links als Gegenspieler zum FAB, nur in WELT. Audio-Element zentral in
index.html → übersteht Welt-Wechsel & Re-Renders.
„Gehört" wird hybrid gemerkt: localStorage (sofort/offline) + DB-Flag
anthem_heard am User (neue Spalte, über /auth/me, gesetzt via
POST /api/profile/anthem-heard) — geräte- und deploy-übergreifend, damit der
Banner nicht erneut nervt.
Wiederverwendbarer UI.noteMediaAttacher für beide Notiz-Stellen (UI.noteModal
+ Notizblock-Seite). note_media-Tabelle + POST/DELETE /api/notes/{id}/media
(vor der gierigen /{parent_type}/{parent_id}-Route). Audio per MediaRecorder,
serverseitig nach m4a/AAC transkodiert (ffmpeg) — iOS spielt Chrome-Opus-webm
nicht ab. UI.lightbox global eingeführt. Mikrofon-Policy microphone=(self) +
CSP media-src 'self' blob:, Datenschutz v6. Disk-Cleanup für note_media bei
Notiz-, Account- und Admin-User-Delete. Reine Medien-Notiz ohne Text erlaubt.
noteModal-Bug gefixt: notes.get() liefert Array -> existing[0] statt
existing?.id (verhinderte Bearbeiten, erzeugte Duplikate). 12 neue Tests.
admin.py enthält außerdem KI-Vision-Statusfelder aus paralleler Arbeit
(nicht sauber trennbar ohne interaktives Staging).
Rene: 'Züchter sollten mehr Einfluss haben — Wurfnamen (B-Wurf), Mitglied-
schaften und Zertifikate fürs Profil.'
- Wurfnamen: Infrastruktur existierte komplett (wurf_rang/wurf_name in DB,
Backend, Wurfverwaltungs-Formular) — war nur im Editor und auf der
öffentlichen Seite unsichtbar. Editor-Karten zeigen jetzt 'B-Wurf · Name'
(Feldname-Bug geburtsdatum→geburt_datum), öffentliche Wurf-Karten bekommen
den Rang/Namen als Badge, Hinweis im Editor verlinkt zur Vergabe.
- 'undefined Medien': my-editor lieferte kein foto_count (+photos fürs
Profil-Grid) — ergänzt.
- NEU Mitgliedschaften & Zertifikate: entity_type 'certificate' im Foto-
System (Ownership wie breeder), Editor-Sektion mit Upload (Bezeichnung als
Caption) + Löschen, öffentliche Profilseite zeigt eigene Sektion mit
Logos/Urkunden (klickbar, lazy). Public-Endpoint liefert result.zertifikate.
Tests: my-editor inkl. Wurfname/foto_count, Zertifikat-Roundtrip bis zur
öffentlichen Seite. Suite: 61 passed.
- Neue Seite #breeder-dashboard (Welten-Chip 'Züchter' role:breeder in HUND,
ersetzt die Einzel-Chips Zuchtkartei + Wurfverw.; beide FABs wandern an den
neuen Chip; Läufigkeit bleibt eigener Chip in HUND, Rene-Vorgabe):
Zwinger-Karte (Name, verifiziert-Badge, Profil-Editor), Wurfverwaltung mit
Wurf-Anzahl, Zuchtkartei mit Hunde-Anzahl. Einzelseiten bleiben erreichbar.
- Settings: Partner-Karte entfernt — der 🤝-Welten-Chip ist der Einstieg.
- Admin 'Aktive Codes': 👥 zeigt jetzt ALLE Einlösungen eines Codes mit
Kanal-Badge (QR #seq aus Kontingent vs. Link/manuell), Datum und
Bestätigt-Status — Endpoint /admin/partner/codes/{id}/registrations.
Suite: 55 passed.
1. QR-URL verrät den Code nicht mehr: /q/{token} → /?qr=TOKEN (vorher stand
der tippbare Code in der Adresszeile jedes Scanners). Registrierung löst
den Code server-seitig aus dem Token auf (auch ohne ref_code).
2. Notbremse: partner_codes.active — Admin kann Codes pausieren (Einlösung
gesperrt, Info-Endpoint 404, Historie/QR-Kontingente bleiben) und
reaktivieren. UI: ⏸/▶-Toggle + pausiert-Badge in der Codes-Tabelle.
3. max_uses im Anlege-Formular standardmäßig 50 statt unbegrenzt.
Tests: QR-only-Registrierung, Pause→keine Einlösung→Reaktivierung,
Redirect ohne Klartext-Code. Suite: 54 passed.
Rene: QR-Stats gehören nicht ins öffentliche Profil, eigene Seite fehlte.
Neue Seite 'Partner-Bereich' (Welten-Chip 🤝 zwischen Moderation und Admin,
role:partner — sichtbar für is_partner + Admin; _mergeDefaults reicht den
Chip an bestehende Welt-Configs nach):
- Einladungscode groß + Link-kopieren-Button
- Kacheln: Registrierungen gesamt / diesen Monat / unbestätigt
- QR-Kontingente mit Einzel-Code-Status (aus partner-profil.js hierher verschoben)
- Profil-Status-Karte (Entwurf/Prüfung/frei) mit Sprung zum Editor
Backend: GET /partner/my-stats (Codes mit Zahlen + Profil-Status).
Settings-Partner-Karte: zwei Buttons (Partner-Bereich primär, Profil sekundär);
Dank-Mail-CTA zeigt auf #partner-dashboard. Suite: 52 passed.
Trigger ist die E-Mail-Bestätigung des Geworbenen (nicht die rohe
Registrierung — konsistent zur Registrierungen/Versuche-Zählung) und nur
beim ersten Verify (Doppelklick auf den Link = keine zweite Mail).
Inhalt: Dank + Bilanz (bestätigte Registrierungen gesamt + diesen Monat),
bei QR-Herkunft der Sticker (#seq, Kontingent-Label), bei Gründer-Codes
die offenen Plätze; CTA zur Partner-Statistik. Versand über das
partner@-Konto, Fehler landen in failed_emails (context partner_thank_you).
Env-Fund dabei: SMTP_PASS fehlte in BEIDEN .env (nur SMTP_SUPPORT_PASS da)
— Partner-Konto-Versand wäre fehlgeschlagen; auf der DS ergänzt.
Test: Mail-Capture per monkeypatch, prüft Statistik + Sticker-Nr +
Einmaligkeit. Suite grün.
Rene: 'wo sieht der Partner welche QR-Codes er hat und wieviele verbraucht
sind?' Neu in 'Meine QR-Codes':
- Kontingent-Zeile zeigt 'X/Y verbraucht' (Codes mit ≥1 bestätigter Registrierung)
- Listen-Button klappt Einzel-Codes auf: #Nr, Kurz-URL, Scans und Status
● verbraucht (mit Registrierungs-Datum) / ◐ gescannt / ○ frei
- Endpoint /partner/my-qr/{id}/codes (owner-gated, keine personenbezogenen
Daten — nur Zähler + Zeitstempel)
Rene: Statistik zählte alles in einen Topf (3 statt 2) und zeigte nicht,
WER sich registriert hat. Jetzt:
- registrations = email_verified=1, attempts = unbestätigte Versuche —
Versuche werden bei späterer Bestätigung automatisch zu Registrierungen
- Admin: 👥-Button pro Kontingent klappt Account-Liste auf (Name, E-Mail,
Datum, ✓ bestätigt/⏳ Versuch, Sticker-Nr #seq) — lazy geladen, admin-only
(personenbezogene Daten); Partner sehen weiter nur Zahlen (Registr. +N)
- Test deckt Versuch→Bestätigung-Übergang und Detail-Endpoint ab
Rene: 'kann nichts prüfen — ich würde den Output erwarten, der auf der
Partner-Seite zu sehen sein wird'. Freigabe-Zeile hat jetzt einen
Vorschau-Toggle, der die Karte 1:1 wie auf #partner rendert (Logo/Initial,
Slogan, Website, Instagram, Bio, Medien-Grid).
Mail-Ursache gefunden: Staging-.env fehlte SMTP_SUPPORT_USER → Code-Default
support@banyaro.de → 535 Auth-Fehler, vom Silent-Catch verschluckt.
.env ergänzt (partner@banyaro.app wie Prod); Submit loggt SMTP-Fehler jetzt
über _log_smtp_failure in failed_emails statt still zu schlucken.
Rene reichte ein Partner-Profil ein und sah als Admin nirgends einen Hinweis:
1. Action-Items kannten Partner-Profile nicht — partner_profiles_pending
(submitted_at gesetzt, approved=0) jetzt im Endpoint + Chip im Admin-Kopf
(Klick -> Partner-Tab). Test ergänzt (7 passed).
2. ADMIN_EMAIL fehlte in BEIDEN .env auf der DS (Prod+Staging) — damit wurden
auch Upgrade-Anfragen-Mails still verschluckt (bekanntes Silent-Skip-Muster).
Auf der DS nachgetragen; greift je beim nächsten Deploy.
Logo-Pfad akzeptierte .heic, öffnete aber direkt mit Pillow (kein HEIF-Opener)
— iPhone-Fotos schlugen fehl. Jetzt convert_media-Vorstufe wie im Foto-Pfad.
Fehlgeschlagene Konvertierungen (HEIC→JPEG, MOV→MP4) brechen mit klarer
Meldung ab statt rohe Dateien zu speichern (MOV wäre als <img> kaputt gerendert).
Test: echter HEIC-Roundtrip (pillow-heif) für Logo + Foto.
Vorher zaehlte NUR eine aufgezeichnete Tour der letzten 48h (<=50m am POI,
>=2 Punkte) — die aktuelle Geraeteposition floss gar nicht ein.
- DogFriendlyIn: optional user_lat/user_lon (Frontend schickt _userPos mit)
- Beleg-Weg a): Geraetestandort <= 75m am POI (50m Radius + GPS-Toleranz)
-> route_id=None markiert Live-Beleg; Weg b) Tour-Beleg unveraendert
- Fehlermeldung nennt jetzt beide Wege (hin gehen ODER Standort aktivieren)
- pytest 39 passed
Bump v1239
Der alte delete_account löschte nur ~6 Tabellen und scheiterte am finalen
DELETE FROM users, sobald der User Zeilen in Non-Cascade-Tabellen hatte
(routes, places, walks, events, forum_threads, invoices …). Jetzt:
PRAGMA defer_foreign_keys + foreign_key_list-Introspektion (CASCADE automatisch,
NO-ACTION-Eigentum löschen, Actor/SET-NULL-Spalten nullen) plus user_id/owner_id-
Scan für Tabellen ohne formale FK. Regressionstest test_account_deletion.py.
Relevant für App-Store-Gl. 4 (In-App-Konto-Löschung muss zuverlässig funktionieren).
_check_post_limits verglich datetime.utcnow() (UTC) mit created_at, das aber
als Client-Lokalzeit (CEST=UTC+2) gespeichert wird → diff negativ → immer 429.
Jetzt rechnen Cooldown + Stunden-Limit in derselben Zeitbasis (Client-Zeit des
Requests). Backend-only, kein SW-Bump.
- update_streak NICHT mehr in GET /achievements/me (lief bei jedem Öffnen).
Streak wird nur noch von create_route (aufnehmen) + record_walk (ablaufen)
hochgezählt.
- Angezeigter Streak korrekt: nur gültig wenn letzte Tour heute/gestern war,
sonst 0 (gerissen) — ohne den gespeicherten Wert zu verändern.
- 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)