- 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.
In-App-Zurück geschlossen wird (Angie-Bug)
Bisher wurde walked_km NUR in _closeNav (In-App-Zurück-Pfeil) gespeichert.
Wer die Navigation anders verlässt (Handy sperren/Home/PWA schließen, oder
nur den Dim-Entsperrpfeil + normal schließen), verlor die km.
- Fortschritt laufend in localStorage sichern (überlebt App-Kill).
- _recordNavWalk() Einmal-Guard, aufgerufen von _closeNav UND pagehide.
- _flushPendingNavWalk() trägt beim nächsten App-Start einen nicht
gespeicherten Walk nach.
- Fehler nicht mehr still verschlucken: bleibt in localStorage → Retry.
- /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.
Apple-Reviewer braucht eine publik erreichbare Support-URL. Die
SPA-Hilfeseite (/#hilfe) ist hinter dem Welcome-Overlay für nicht
angemeldete User versteckt. Neue /help-Route rendert serverseitig:
- Holt aktive FAQ-Artikel aus help_articles (über bestehendes
TTL-Cache _load_active_help_articles).
- Gruppiert nach Kategorie mit deutschen Labels.
- Native HTML5 <details>/<summary> Akkordeon — kein JS nötig.
- Dark Mode via prefers-color-scheme.
- Direkter mailto support@banyaro.app + Verweis auf die volle
Hilfe nach Login.
Damit haben wir https://banyaro.app/help als Support-URL für App
Store Connect.
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.
Bisher: wenn eine Route Fotos hatte, zeigte die Karten-Übersicht das
erste Foto statt der Mini-Map → kein Vergleich der Tracks auf einen
Blick möglich.
Jetzt: die Mini-Map ist immer da, ein kleiner Kamera-Badge oben rechts
zeigt die Anzahl Fotos an (analog zum Tagebuch). Die Fotos sind weiter
unverändert in der Route-Detail-Ansicht zu sehen.
CSS: .rk-card-preview bekommt position:relative für das Badge.
- 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.
Bisher: App.navigate() rief nur dann Worlds.hide() (und damit
worlds-back-visible) wenn Worlds gerade sichtbar war. Wer aus dem
Onboarding direkt nach #dog-profile navigiert (kein vorheriges
Worlds-Anzeigen) hatte keinen Zurück-Pfeil zu den Welten + FAB —
saß auf dem Profil fest.
Fix: in navigate() unabhängig vom Worlds-State die Klasse
worlds-back-visible setzen, sobald ein eingeloggter User auf einer
nicht-welcome/onboarding-Seite ist. Bump 1136→1137.
- 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.
Wer aus dem Onboarding (Schritt 1 'Los geht's' navigiert direkt auf
#dog-profile) keinen Hund anlegen will, war bisher in der Form
festgehängt — kein Skip, kein Zurück.
Jetzt: ghost-Button unter dem Submit, setzt by_onboarding_done und
schickt zurück auf die Welten/Welcome. Bumpe auf 1135.
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.
User-Report: Zeilenumbrüche in Notes-Karten gingen nicht, kein Scroll,
keine Detail-Ansicht.
Drei Probleme behoben:
1. _truncate-Limit zu aggressiv (150 Zeichen)
→ erhöht auf 600 Zeichen damit Karten lange Notizen mit Newlines
sichtbar anzeigen können (CSS-Clamp erledigt visuell den Rest)
2. .list-item-text + .notes-card-text Override-Konflikt
list-item-text hat fest -webkit-line-clamp:2 mit display:-webkit-box.
Notes-Override hatte display:block — das deaktiviert clamp komplett,
aber dann zeigt der Text die ersten 150 Zeichen ohne Newline-Hinweis.
→ Neuer Override: display:-webkit-box + -webkit-line-clamp:5 +
white-space:pre-wrap → 5 Zeilen mit Newlines sichtbar, Rest '…'
3. Keine Detail-Ansicht beim Klick auf Karte
→ Neue Funktion _openDetailModal(note):
- Voller Notiz-Text scrollbar (.notes-detail-text mit max-height:60vh)
- Rubrik-Icon + Label im Titel
- Parent-Label, Micro-Badges, Meta (Zeit + Ort)
- Footer: 'Bearbeiten' (öffnet Edit-Modal) + 'Schließen'
→ Card-Click bindet darauf; Klicks auf Action-Buttons werden via
closest('.list-item-action-btn') ignoriert (kein doppeltes Handling)
Container startete mit USER appuser nicht: SQLite gibt
'attempt to write a readonly database' — Synology DSM Volume-
Permissions blockieren chown auf gemountete Pfade.
User-Anlage (groupadd/useradd) bleibt im Dockerfile, plus
chown nach mkdir. Nur die USER-Zeile ist auskommentiert mit
Kommentar warum. Für Non-DS-Deployments einfach Zeile
aktivieren.
VAPID-Keys-Migration bleibt — die war erfolgreich.
1. VAPID-Keys aus docker-compose.yml und docker-compose.staging.yml
entfernt. Werden jetzt aus .env gelesen (env_file war schon da,
nur die environment-Override hat die .env-Werte überschrieben).
.env auf DS um die 3 Keys ergänzt:
- VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_CONTACT
Erst Compose-Änderung wirksam — Push-Notifications funktionieren
weiter weil die .env die Werte liefert.
2. Dockerfile-Hardening: Non-root USER appuser.
- groupadd/useradd appuser (UID/GID 1000 für DS-Kompatibilität)
- chown -R appuser:appuser /app /data nach mkdir
- USER appuser vor CMD
Memory says DSM ACLs könnten Probleme machen. Falls Container
nicht startet → Rückbau. Bei Deploy genau hinsehen.
3. E-Mail-Änderungs-Audit-Punkt: kein Vulnerability gefunden.
ProfileUpdate-Schema enthält kein 'email'-Feld. User können
ihre E-Mail-Adresse aktuell gar nicht ändern → kein Takeover-
Vektor wie im Audit vermutet.
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.
Bündel 1 aus dem Duplikat-Audit: existierende zentrale Helper nutzen
statt lokale Duplikate.
Pure Migration ohne neuen Code:
- 1167 _esc()-Aufrufe in 36 Page-Modulen migriert auf UI.escape()
- 24 lokale _esc/_escape-Definitionen entfernt
- lost.js hatte _escape() (Variante) — 17 Aufrufe ebenfalls migriert
- jobs.js + breeder.js: tote Alias-Wrapper entfernt
UI.escape() existierte schon — wurde nur überall lokal nochmal
implementiert. Funktional identisch (gleiche 4-replace-chain für
& < > ").
Tests 19/19 grün. Frontend-LOC um ~120 Zeilen reduziert.
Hinweis: _emptyState (7 Stellen) und _icon (8 Stellen) wurden NICHT
migriert — sie haben abweichende Signaturen von UI.emptyState({...})
bzw. UI.icon(name). Eigener Sprint nötig.
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.
User-Report: trotz onerror-Fallback weiter Fragezeichen.
Ursache: Das _preview.webp-System wurde damals nur konsequent für
Diary-Uploads ausgerollt. User-Avatare und Hund-Profilbilder haben
keine Preview-Variante → 404 vom _preview triggert kurz das
Browser-Default-Broken-Image-Icon BEVOR der onerror-Fallback das
Original lädt (Race-Condition).
Pragmatischer Fix: Preview-System in friends.js rückgebaut. Bilder
werden direkt mit Original-URL geladen. Performance kommt durch:
- loading=\"lazy\" (off-screen Bilder erst beim Scrollen)
- decoding=\"async\" (Main-Thread bleibt frei)
- onerror=\"this.style.display='none'\" (kaputte Bilder verschwinden
statt Fragezeichen zu zeigen)
UI.previewUrl + UI.previewFallback bleiben als Helper verfügbar
für später falls das Preview-System app-weit ausgerollt wird.
User-Report: nach Sprint-Migration auf _preview.webp tauchen
Fragezeichen-Icons auf — wenn weder Preview noch Original verfügbar.
Probleme im vorigen Fix:
- UI.escape() ist HTML-Escape, kein JS-String-Escape → URL mit
?param=value wurde &-encoded und damit kaputt
- 'opacity:0.3' lässt das Browser-Default-Broken-Image-Icon
durchscheinen (Fragezeichen sichtbar)
- Kein Loop-Schutz beim onerror
Fixes:
- String-Escape via .replace(/'/g, \"\\'\") statt UI.escape()
- display:none + .img-broken-Klasse bei finalem Fehler
- dataset.fb='1' verhindert Endlos-Loop wenn Original-URL auch 404
- Wenn URL nicht mit /media/ startet: direkt ausblenden (keine
Preview-Variante zu probieren)