Commit graph

295 commits

Author SHA1 Message Date
6ea3f50b05 Forum: idempotente Antworten gegen Doppelpost/Cooldown-Fehler bei Funkloch (v1306)
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).
2026-06-19 10:29:42 +02:00
140140f690 Tagebuch: manuelle Positionierung reichert POIs + Wetter an (v1305)
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).
2026-06-18 21:13:47 +02:00
ac0814e687 Forum: Anpinnen pro Thema/global + Admin-Berechtigung (v1303)
- 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).
2026-06-18 20:36:06 +02:00
d0a76e1b54 Ban Yaro Blues — Hymne in der WELT-Welt
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.
2026-06-14 21:33:23 +02:00
f7370028da KI-Vision-Model, Breed-Scraper, Karte/Routen + Release v1292
Parallele Arbeit (auf Staging mitgetestet): KI-Vision-Model (VISION_MODEL in
ki.py/routes, im KI-Status sichtbar), Breed-Scraper-Anpassungen
(breed_enricher/breed_evaluator, evaluate_enrichment mit user_id),
Karten-/Routen-Änderungen (map.js, routes.js), kleinere UI-Anpassungen
(admin.js, components.css), docker-compose, MARKETING, nav-loop-Test.

Version-Bump auf 1292 (VERSION, sw.js, app.js, index.html, landing.html).
2026-06-14 20:23:21 +02:00
e86d89f3d9 Notiz-Medien & Sprachnachrichten: Fotos/Videos/Dateien + Audio an Notizen
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).
2026-06-14 20:22:35 +02:00
bf5df11f78 iOS-Voll-App M0: Media-Registry (iCloud-Hybrid) — Originale in Nutzer-CloudKit, Server nur Previews: POST/PATCH/GET /api/media (register/confirm/mine/original-Fallback), Phantom-URL+iCloud-404 in serve_media, Registry-Cleanup in Delete-Pfaden, media_items mit storage+ck_record_name; Datenschutz v5 (CloudKit); Fixes: daily_photo_cache in zentrale Migration (Löschen warf auf frischer DB 500), Preview/Thumb-Leichen beim Medium-Löschen; 9 neue Tests, Suite 73 grün 2026-06-10 19:58:30 +02:00
60fb866283 Gründer-Tickets: 50%-Rabatt-Weitergabe pro Gründer gedeckelt + Pro-Wording korrigiert
Rene: 'ungern jemandem auf ewig die Möglichkeit geben 50% Rabatt zu vergeben —
bei 100 Gründern ein großer Faktor. Ich hätte jedem 25–50 Tickets gegeben.'

- users.founder_referral_tickets (Default 25): Kontingent an 50%-Rabatten,
  die ein Gründer an Geworbene weitergeben kann. Technisch = die ersten N
  VERIFIZIERTEN Geworbenen (nach Anmeldedatum) bekommen 50%, danach 0.
  Unbestätigte verbrauchen kein Ticket. In scheduler.py (Rechnung) + admin.py
  (Vorschau) konsistent.
- BUGFIX nebenbei: admin.py zeigte für referred_by_founder fälschlich 100%
  statt 50% (scheduler war korrekt) — jetzt beide 50%.
- Admin: Grant-Formular bekommt Feld 'Gründer-Tickets' (0–200, Vorbelegung
  aus User-Stand); Endpoint /grant akzeptiert founder_tickets.
- Gründer-Seite + Settings + Admin-Hilfe: 'sobald Bezahlfunktionen aktiv sind'
  raus (Pro kostet bereits); Vorteil 'lebenslang Pro gratis' + '25 Freunde
  zum halben Preis' (Ticket-Framing).
- Tests: test_founder_tickets.py (Cap, Unverified-Schutz, 50%-Bugfix, Grant).
  Suite: 64 passed.
2026-06-08 06:20:19 +02:00
5f01abc590 Züchter-Editor: Wurfnamen sichtbar, 'undefined Medien' gefixt, Mitgliedschaften & Zertifikate
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.
2026-06-07 21:00:14 +02:00
487dacc7c7 Fix: /breeder/my-editor Endpoint (Crash 'Cannot destructure profile') + Läufigkeit in Züchter-Bereich
breeder-editor.js (aus 459cd42) rief /api/breeder/my-editor auf — Endpoint
existierte nie (gleicher Worktree-Verlust wie /partner/my-profile). Jetzt
gebaut: profile + litters + storage_mb/limit; ohne Profil klare 404 statt
Destrukturierungs-Crash. 3 Tests.

Läufigkeit (Rene): eigener HUND-Chip entfällt, stattdessen vierte Karte im
Züchter-Bereich (Zyklen, Progesterontests, Deckdaten). Suite: 58 passed.
2026-06-07 20:04:43 +02:00
ed7c469c6a Züchter-Bereich (Hub) + Settings-Partner-Karte raus + Admin: alle Code-Einlösungen mit Kanal
- 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.
2026-06-07 19:55:51 +02:00
2927ae2672 Schutz gegen kursierende Partner-Codes (Rene: 'Bonus-Codes kursieren gerne das Internet')
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.
2026-06-07 19:35:31 +02:00
0a262989f3 Feature: Partner-Dashboard (#partner-dashboard) — operative Daten raus aus dem Profil-Editor
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.
2026-06-07 19:06:51 +02:00
3d7d5dc1c4 Feature: Dank-Mail an Partner bei bestätigter Registrierung — mit Statistik
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.
2026-06-07 18:51:54 +02:00
df2f42f8ac Partner-Self-Service: Einzel-Code-Status — welcher Sticker ist verbraucht?
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)
2026-06-07 18:46:54 +02:00
970480c1d6 QR-Stats: Registrierungen (bestätigt) vs. Versuche (unbestätigt) + Account-Detail-Liste
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
2026-06-07 18:43:18 +02:00
f604ab7c4f Feature: QR-Kontingente für Partner — Bestellung, Übergabe, Rückverfolgung
Partner verteilen gedruckte QR-Codes (Sticker/Flyer); jeder physische Code
ist einzeln rückverfolgbar von Scan bis Registrierung.

Backend:
- partner_qr_batches + partner_qr_codes (Token 8-stellig, ohne 0/O/1/l/I),
  users.referred_qr, partner_codes.owner_user_id (+Backfill über referred_by)
- /q/{token}: Scan zählen (scans, first/last_scan_at) → Redirect
  /?ref=CODE&qr=TOKEN — dockt am bestehenden Referral-Flow an
- Registrierung: qr_token wird nur zugeordnet, wenn er zum eingelösten
  Partner-Code gehört (Manipulationsschutz)
- Admin: Kontingent bestellen (max 500), Liste mit Scans/Registrierungen,
  Löschen (Zweiklick), druckfertiges A4-PDF (segno+fpdf2, 3×4 Grid mit
  Kurz-URL + laufender Nummer), Code-Besitzer zuordnen
- Partner-Self-Service: /partner/my-qr (+PDF) für Code-Besitzer

Frontend:
- Admin-Partner-Tab: Karte 'QR-Kontingente' (Bestellung, Stats, PDF, Besitzer)
- Partner-Profil: 'Meine QR-Codes' mit Scans/Registrierungen + PDF-Download
- boot.js/app.js speichern ?qr=, Registrierung schickt qr_token mit

Neu: segno==1.6.6 (pure-python QR). Tests: 5 neue (PDF, Scan-Zählung,
Attribution, Fremd-Token-Schutz, Self-Service). Suite: 51 passed.
2026-06-07 18:20:23 +02:00
cadfb24a8d Partner-Freigabe: Live-Vorschau im Admin + Mail-Fehler sichtbar machen
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.
2026-06-07 17:43:42 +02:00
a40aa183ec Admin: offene Partner-Profil-Freigaben in 'Zu erledigen'-Leiste + ADMIN_EMAIL-Befund
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.
2026-06-07 17:34:56 +02:00
8a614eef1a Fix: HEIC/MOV-Konvertierung bei Partner-Uploads
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.
2026-06-07 17:29:59 +02:00
ce8aa2b699 Feature: Partner-Profile Backend + Pro-Zugang für Partner
Die Partner-Showcase-Seite (#partner) und der Profil-Editor (#partner-profil)
existierten seit v1102 nur als Frontend — /api/partners/public und
/api/partner/my-profile gab es nie (vermutlich Worktree-Merge-Verlust).

Backend neu:
- partner_profiles-Tabelle (user_id PK, ON DELETE CASCADE → DSGVO-Delete greift)
- GET/PUT /partner/my-profile (Texte, Website-Normalisierung, @-Instagram)
- Logo-Upload (≤5 MB → WebP 512px, altes Logo wird geräumt)
- Foto/Video-Upload (max 6, 200-MB-Budget, HEIC→JPEG, MOV→MP4 via ffmpeg,
  Bilder→WebP 1600px) + Lösch-Endpoint
- Submit-Workflow (approved 0/1/-1) + Admin-Mail (best effort)
- GET /partners/public (nur freigegebene, JOIN users für Name/Avatar)
- Admin: GET /admin/partner/profiles + POST .../review

Pro für Partner: has_pro_access() + App._hasPro() prüfen jetzt is_partner —
Multiplikatoren bekommen Pro gratis (mehrere Hunde, KI-Trainer etc.).

UI: Admin-Partner-Tab mit Freigabe-Sektion (offen-Badge, ✓/✗),
Settings zeigt Partnern eine Karte mit Link zum Profil-Editor.

Tests: tests/test_partner_profile.py — 5 Smoke-Tests (403, Voll-Flow
inkl. Freigabe/Ablehnung, Pflicht-Anzeigename, Logo+Foto-Upload, Pro-Zugang).
Suite: 44 passed.
2026-06-07 17:20:20 +02:00
2042a3f513 Routenvorschlaege: Schwierigkeit aus Distanz+Hoehenmetern + Yaro-Navi mit echtem Gebell
Schwierigkeit (Wunsch Rene): ORS elevation=true -> ascent; leicht <4km & <50hm,
mittel <=7km & <150hm, sonst anspruchsvoll (vorher NUR km — flache 6km galten
als 'anspruchsvoll'). ACHTUNG: elevation=true codiert die Polyline 3D —
eigener _decode_polyline3d (Roundtrip-getestet), Track enthaelt jetzt alt;
Hoehenmeter im Vorschlag als Pill ('X hm') + in der API (hoehenmeter).

Navi-Sounds: echtes Schaeferhund-Gebell (Renes Aufnahme zugeschnitten):
/sounds/wuff.mp3 (0,34s Einzel-Beller, 2x=links 1x=rechts) +
/sounds/klaeffen.mp3 (2,8s Sequenz, falscher Weg — spielt 1x statt 4x);
mono, loudnorm -14 LUFS, Fades. /sounds-Mount (main.py), SW-Precache
(bellt auch im Funkloch). Synthese bleibt Fallback.
pytest 39 passed. Bump v1243
2026-06-06 19:51:20 +02:00
6a06c9be7e POI-Bewertung: Live-Praesenz zaehlt als GPS-Beleg (Rene stand am Ort, wurde abgewiesen)
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
2026-06-06 17:26:20 +02:00
545b57c723 Fix: Account-Löschung FK-sicher über alle Tabellen (defer_foreign_keys + Introspektion)
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).
2026-06-04 19:21:18 +02:00
959fd81a9b Fix: Forum-Cooldown blockierte JEDEN Post (Zeitzonen-Bug)
_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.
2026-06-04 16:50:45 +02:00
f5b5bb2289 Streak: entkoppelt vom App-Öffnen — nur noch echte Touren zählen
- 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.
2026-06-04 09:40:12 +02:00
667ed91f33 Routen: Bestätigungs-Toast nach Ablaufen + "X× gelaufen · zuletzt" auf der Karte
- 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.
2026-06-04 09:34:03 +02:00
684ffa3b46 OSM-Verknüpfung: In-App-Hilfe „Konto erstellen" (umgebungsabhängig)
- /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.
2026-06-03 22:04:42 +02:00
b4cafc0363 OSM-Verknüpfung: Post-Link-Redirect auf Einstellungen (/#settings) statt App-Root 2026-06-03 22:02:42 +02:00
9afbf24535 OSM-Beiträge: "Hund willkommen?" 👍/👎 (dog=yes/no) + Umdrehen
- 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.
2026-06-03 21:49:44 +02:00
57849515ea OSM-Beiträge: Map-Button (dog=yes), Changeset-Upload, Confirm/Pro-Job
- Map-Popup: "Hund war willkommen"-Button (dog=yes) für Restaurant/Hotel/
  Shop/Tierarzt/Hundesalon → POST /osm-contrib/dog-friendly.
- OSM-Changeset-Upload (write_api): Element holen (node/way) → dog=yes →
  Changeset create/upload/close; idempotent; best-effort beim Tap.
- OSM-Endpunkte konfigurierbar (OSM_OAUTH_BASE/OSM_API_BASE) — Staging gegen
  Dev-Sandbox, KEINE echten Edits auf Produktiv-OSM.
- Scheduler-Job (täglich 03:40): Pending-Retry + Revert-Überleben (7 Tage) →
  confirmed/rejected; Pro-Freischaltung (100 confirmed = 1 Jahr, idempotent via
  osm_pro_grants). HINWEIS: is_premium/subscription direkt gesetzt — vor Prod
  mit Billing abgleichen.
- Native Attestierung/Sensoren: bewusst NICHT (iOS-App-Thema, nicht PWA).
2026-06-03 21:40:50 +02:00
1cfaa0264f OSM-Beiträge: dog=yes-Erfassung mit GPS/Zeit-Anti-Fraud + Gamification-Zähler
- Tabelle osm_contributions (status pending→submitted→confirmed/rejected).
- Router /api/osm-contrib: POST /dog-friendly (Anti-Fraud: GPS-Beleg über
  kürzliche eigene Tour ≤50m + Verweil-Proxy, Tour-Recency 48h, Tages-Cap,
  Dedup, Positions-Sanity), GET /status (Zähler).
- Settings-UI: Zähler "X Orte eingetragen · noch Y bis Badge/Pro".
- OSM-Changeset-Upload + Pro-Freischaltung + Geräte-Attestierung folgen separat.
2026-06-03 21:20:32 +02:00
46caa05020 OSM-Verknüpfung (Modell A): OAuth2-Fundament für Nutzer-Beiträge
- Tabelle user_osm (access_token verschlüsselt at rest via Fernet,
  Schlüssel aus JWT_SECRET abgeleitet oder OSM_TOKEN_KEY).
- Router /api/osm-auth: authorize (signierter state mit user_id+CSRF),
  callback (Code-Tausch + OSM-Name holen + speichern), status, unlink.
- Profil-UI (Settings): "OSM-Konto verknüpfen" / verknüpft-als / trennen,
  hundehalter-spezifische Motivation.
- cryptography in requirements.
- Basis für dog=yes-Beiträge + Gamification/Pro (folgt). Staging-Branch.

ENV nötig: OSM_CLIENT_ID, OSM_CLIENT_SECRET (Redirect-URI default staging).
2026-06-03 21:14:36 +02:00
4bc7454258 POI-Karte: Offline-Import aus OSM statt Live-Overpass-Scan (Build 4)
- osm.py: Live-Scanner deaktiviert — /pois liest nur noch aus DB,
  /analyze ist No-Op. Behebt wiederholte OSM-Banns (Tile-Load + Scanning).
- tools/osm-extract: Extraktion (pyosmium) + Loader (schützt user_edited)
  + Docker-Refresh-Job mit osmium-tags-filter-Vorstufe (RAM-schonend).
- docker-compose.osm.yml: Refresh-Service (mem_limit 4g), monatlich via
  DSM-Aufgabenplaner.
2026-06-03 20:44:32 +02:00
10e39ed135 Karten-Ausbau (OSM), Forum-Erweiterung, UI-Komponenten, Refactor Tagebuch/Gassi (DRY), Landing/SEO — APP_VER 1155 2026-06-03 17:24:47 +02:00
d23d696745 Tagesfoto-Cache validieren + bei Diary/Media-Löschung mit-bereinigen
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.
2026-05-30 19:00:56 +02:00
4cfce1051f Pro-Badge im Profil: zeigt jetzt korrekten Tier statt veraltetem is_premium
- 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.
2026-05-30 18:10:57 +02:00
bd9acda084 User-Löschen: Upgrade-Anfragen + Hund-Daten mit aufräumen (Admin + Self-Delete)
- 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.
2026-05-30 17:51:24 +02:00
0685f3b8ef Welcome-Dashboard: Tagesfoto wird serverseitig pro Tag gecacht
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.
2026-05-30 12:55:48 +02:00
9b0c286aa3 Expenses: GET /api/expenses/categories liefert die Kategorien
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.
2026-05-30 12:31:59 +02:00
7b3041fc94 DRY: Notiz-Modal zentral in UI.noteModal (11 divergierte Kopien entfernt, ~750 Z. weniger); Fix: Founder-Race in jobs.py atomar + founder_number, SW v1133 2026-05-29 10:51:42 +02:00
a356626d39 Feature: Pflege-Routinen (Zecken-/Flohschutz, Krallen, Fellpflege) — neuer Pflege-Tab mit Erledigt+Auto-Wiedervorlage, Push-Erinnerungen, intervall_tage-Fix im INSERT, SW v1132 2026-05-29 10:32:05 +02:00
26b515cede Fix: Anniversary-Job + RASFF 404, SW by-v1120
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.
2026-05-27 14:51:34 +02:00
1ff66a7083 Sicherheit + Tests + A11y, SW by-v1118
PYDANTIC max_length (38 Routen, ~400 Field-Constraints):
Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.).
Pragmatische Limits:
- Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000
- Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100
- Hund-Name/Rasse: 80 · Hund-Bio: 2000

Top-betroffen: forum.py, diary.py, health.py, dogs.py, expenses.py,
notes.py, auth.py, profile.py. Manuelle len()-Checks in profile,
chat, ki entfernt (jetzt durch Field abgedeckt).

PYTEST COVERAGE (+19 Tests, 37 grün + 1 xfail):
- test_security.py: require_owner (Places GET/PATCH/DELETE mit
  Fremduser → 403), JWT-Blacklist (Logout invalidiert Token),
  Login-Lockout (5 Fehlversuche → 429 + Retry-After Header)
- test_race.py: Invoice-Counter (20 parallele Threads, alle unique),
  Founder-Number (atomare Vergabe, voll bei 100)
- test_validation.py: Forum-Titel 30k Zeichen → 422, Diary-Text
  50k → 422 (verifiziert Pydantic max_length-Sweep)

A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast):
- #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44
- dog-profile Wrapped-Slider Prev/Next 40→44
- forum-Lightbox Close 40→44
- --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS)
- --c-text-muted Dark:  #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS)
- Branding-Farben unangetastet
2026-05-27 13:40:30 +02:00
35937ed51b Bündel 3: Security-Helper + Demo-Migration, SW by-v1115
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.
2026-05-27 11:27:00 +02:00
297bd22f96 Bündel 2: Zentrale Helper für DRY-Cleanup, SW by-v1114
NEUE BACKEND-MODULE:

math_utils.py
- haversine_km(lat1, lon1, lat2, lon2) — Distanz in km
- haversine_m(...) — Convenience-Wrapper in Metern
- bbox_deg_from_km(lat, radius_km) — Bounding-Box-Approximation
  für SQL-Vorfilter (statt Haversine im Python-Loop)

config.py
- DB_PATH, MEDIA_DIR, BREEDER_DOCS_DIR, SCANINPUT_DIR
- API_TIMEOUT_SHORT (5s) / DEFAULT (10s) / LONG (30s)
- HTTP_USER_AGENT, HTTP_HEADERS

errors.py
- not_found(msg), forbidden(msg), bad_request(msg), unauthorized(msg)
- conflict(msg), too_many_requests(msg, retry_after), service_unavailable(msg)
- require_or_404(row, msg) — Convenience-Helper

UI.JS ERWEITERUNGEN:

UI.time erweitert:
- formatDate(d)     → "15.03.2026"
- formatDateTime(d) → "15.03.2026, 14:30"
- weekday(d)        → "Di"
- parseISO(str)     → {year, month, day}

UI.text (neu):
- truncate(str, maxLen, ellipsis='…')
- slug(str) — URL-Slug aus String (mit DE-Umlauten)

UI.money (neu):
- format(value) → "12,34 €" (de-DE, EUR)
- formatWithSuffix(value, '/Jahr')

HAVERSINE-MIGRATION (13 Backend-Routen):
alerts.py, services.py, places.py, events.py, diary.py, playdate.py,
lost.py, poison.py, adoption.py, gassi_zeiten.py, sitting.py, routen.py,
walks.py

- Alle lokalen def _haversine/haversine_km entfernt
- Aufrufe ersetzt durch haversine_km/haversine_m je nach Einheit
- from math_utils import haversine_km|haversine_m in jeder Datei

Tests 19/19 grün.

Hinweis: Migrationen für MEDIA_DIR (19 Stellen), API-Timeouts (12),
Date-Formatter im Frontend (24) und UI.text.truncate (5) sind als
Folge-Sprints möglich. Helper sind verfügbar.
2026-05-27 11:19:06 +02:00
e7939ce98e Bündel A-D: Race-Fixes, JWT-Cleanup, Storage-Watchdog, HTTPException, SW by-v1112
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.
2026-05-27 09:41:56 +02:00
15d319fbd5 Admin: POI-Statistik erweitert (Nutzer-POIs nach Typ + Labels), SW by-v1099
- /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
2026-05-26 21:37:35 +02:00
cc4f030fd0 Feature: Hundesalons in der Karte, SW by-v1098
- 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)
2026-05-26 21:13:16 +02:00
9394bab1fb Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py)
- JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein,
  decode_token() prüft → server-side Invalidierung
- JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt)
- Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht
  (Schwelle via JWT_REFRESH_FRACTION, Default 2)
- Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min,
  überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt
- SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt;
  Fehlversuche landen in failed_emails-Tabelle für späteres Retry
- Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1
  ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE

RACE CONDITIONS (routes/invoices.py, database.py)
- Neue invoice_counters-Tabelle für atomare Nummernvergabe
- _next_invoice_number nutzt BEGIN IMMEDIATE + atomares UPDATE
- Funktioniert für RG- und ST-Prefixe (Stornorechnungen)
- Race-Test verifiziert (5 Threads × 20 Calls = 100 eindeutige Nummern)

VERSION + TESTS + ERROR-DIGEST (VERSION, Makefile, tests/, scheduler.py)
- Neue VERSION-Datei (Single Source of Truth) — main.py liest beim
  Startup
- Makefile-Target 'make bump' propagiert in sw.js, app.js, index.html
- Makefile-Target 'make test' setzt venv auf, läuft pytest
- 19 Smoke-Tests in tests/ (health, auth, diary, invoice) — alle grün
- Scheduler: täglicher _job_error_digest um 06:30 → schickt Error-
  Zusammenfassung an ADMIN_EMAIL (still wenn keine Errors)

DSGVO + A11Y + ERSTE-HILFE
- landing.html: 'HTML und ODS' → 'JSON' (tatsächlich implementiert)
- datenschutz.js: Sektion Account-Löschung erweitert (sofort gelöscht /
  anonymisiert / 10 Jahre für Rechnungen)
- erste-hilfe.js: prominentes Warning-Banner oben (ersetzt keine
  Tierarzt-Beratung); Notfallnummern gruppiert nach Land, TODO-Platz-
  halter für AT-Uni-Klinik, CH Tox Info Suisse, CH Tierspital Zürich
- ui.js Modal: ESC schließt, Focus-Trap, Auto-Focus erstes Element,
  Restore Focus auf vorigen Caller
- impressum.js Kontaktformular: Labels mit for=cf-name etc.

NEUE DB-TABELLEN (idempotent via CREATE TABLE IF NOT EXISTS)
- jwt_blacklist, login_attempts, failed_emails, invoice_counters

NEUE ENV-VARS
- JWT_REFRESH_FRACTION (Default 2)
- JWT_EXPIRY_DAYS Default geändert (30 → 7)
2026-05-26 20:12:01 +02:00