Commit graph

517 commits

Author SHA1 Message Date
d96fa9e24e Seitenkarten destroy(): GL-Karte beim Seitenwechsel freigeben (WebGL-Kontext-Leak)
poison/lost/walks/events: destroy() ruft _map.remove() → app.js gibt den WebGL-Kontext beim
Navigieren frei. Sonst akkumulieren Kontexte → iOS-Limit (~8) → neue GL-Karten (z.B. Routen-Detail)
scheitern → Leaflet-Raster-Fallback.
2026-06-05 13:16:38 +02:00
27a3f954a4 Routen-Fixes: Richtungspfeile (SVG-interne Rotation) + Filter standardmäßig zu
- Pfeile: rotate als SVG-Pfad-Attribut statt CSS-transform am Element (maplibregl.Marker
  überschrieb das transform → Pfeile zeigten alle nach Norden)
- Filter-Panel + Badge: doppeltes class-Attribut (class=X class=hidden) → Browser ignorierte
  'hidden' → Filter immer offen + roter Badge immer an. Zu 'X hidden' gemergt.
2026-06-05 13:13:14 +02:00
fbaf7c5409 Routen-GL-Fix: Detail/Suggest-Karte fittet Route korrekt (Modal-0×0-Timing)
- Facade fitBounds: try/catch + skip wenn Container 0×0 (sonst NaN-LngLat im Modal)
- createMap: mehrfaches resize() nach Erstellung (Modal-Animation)
- _buildDetailMap/_suggestMap: Re-Fit nach 200/500ms (Route ganz sichtbar, Pfeile)
- Facade: scrollWheelZoom-Stub (map.scrollZoom)
2026-06-05 13:01:19 +02:00
96119e02ef Seitenkarten GL Runde 2: Events, Gassi, Routen + Facade-Erweiterung
- Facade: Polyline (geojson line-source, addTo/setLatLngs/getBounds/remove), clusterGroup,
  marker.getLatLng, map.distance(Haversine), on('click') normalisiert e.latlng aus e.lngLat, _ll objekttauglich
- events: L.markerClusterGroup→UI.map.clusterGroup
- walks: window.L-Guard, L.featureGroup→UI.map.featureGroup, fitBounds ohne .pad
- routes: L.polyline/L.circleMarker→UI.map.*, navMap/Pfeil-Marker→svgMarker, latLngBounds→coords,
  trimMap distance/click, Mini-Vorschauen auf SVG (kein WebGL-Limit, kein OSM-Raster)
2026-06-05 12:48:09 +02:00
5844f1ef51 Seitenkarten auf MapLibre GL (Facade) — Runde 1: Giftköder + Verlorene
- map-gl-mini.js: Leaflet-kompatible MapLibre-Facade (createMap/svgMarker/circleMarker/
  featureGroup-Wrapper mit setView/fitBounds/invalidateSize/addTo/bindPopup/openPopup/on/remove)
- ui.js: UI.map.create/svgMarker/leafletMarker branchen auf GL (by_map_gl, Staging-Default),
  + UI.map.circleMarker/featureGroup, loadMapLibreUI
- poison.js/lost.js: window.L-Guards entfernt, L.circleMarker→UI.map.circleMarker
2026-06-05 12:33:01 +02:00
3523a44a0b MapLibre: GL als Staging-Default + Feinschliff (Cluster-Zahlen, Theme-Robustheit)
- _useGL: Staging default-AN (Prod aus, ?mapgl=0 überschreibt) → Breitentest
- Cluster zeigen ZAHL (point_count) statt Icon (Glyphs vorhanden)
- Theme-Wechsel: Wetter-Raster + Rec-Track nach setStyle neu anlegen; Click-Handler nur einmal binden (keine doppelten Popups)
2026-06-05 11:26:55 +02:00
425f99effb MapLibre-Port 3b+3c: Wetter + GPS-Aufzeichnung für GL entgaten
3b Wetter: engine-neutrale Helfer (_wxAddRaster→raster-source unter den Markern, _mapOnMove,
   _wxAddTempMarker→maplibregl.Marker). _toggleRadar/_loadRadar/_toggleTemp/_loadTempLabels gebrancht.
3c GPS: _recTrackGL (geojson line-source), _updateRecMarker (maplibregl.Marker), _recCleanupMap.
   _updateRecMap/_startRecording-Resume/Cleanup gebrancht, Gates entfernt. panTo [lng,lat] für GL.
2026-06-05 11:19:40 +02:00
9c959dd632 GL-Karte: Ortsnamen-Labels (Glyphs self-hosted) + ScaleControl raus (lag unter der Status-Pill)
- main.py: /fonts-Mount (Glyph-PBFs aus data/tiles/fonts), Open Sans Regular self-hosted
- map-gl-style.js: glyphs-URL + Label-Layer (Ortsnamen/Straßen/Gewässer, name:de)
- map.js _initMapGL: ScaleControl entfernt (überdeckte die Zoom/Wetter/Zecken-Pill)
Ortsnamen für Orientierung (René), auch bei kleinem Zoom.
2026-06-05 11:09:08 +02:00
4d0cd0f460 Karten-Fix: fraktionalen MapLibre-Zoom für Scan-Schwelle runden
MapLibre-Zoom ist kontinuierlich (13.7); Statusleiste rundet (zeigt Z14), Scan verglich roh
→ bei angezeigtem Z14 (echt 13.x) galt zoom<14 → Marker gecleart, erst ab Z15 sichtbar.
Jetzt Math.round(getZoom()) für die 10/14-Schwellen (wie die Anzeige). Leaflet unverändert (ganzzahlig).
2026-06-05 11:01:09 +02:00
980338d7f1 Karten-Fix: Scan-Race bei schnellen Zoom-Folgen (Z16→Z13→Z14 → keine Marker)
_loadOsmLayers verwarf Scan-Anfragen, die während eines laufenden Scans kamen (return).
Bei langsamem Overpass (Handy) ging so der finale Z14-Scan verloren → Marker leer.
Jetzt: _scanQueued merkt die Anfrage vor, finally holt sie nach → letzte Ansicht wird garantiert gescannt.
2026-06-05 10:56:08 +02:00
2d7eca16a7 MapLibre-Port 3a-Fix: Scanner-Endlosschleife (resize im Scan) + Pinch-Page-Zoom + Rotation
- _loadOsmLayers: kein _map.resize() für GL (löste move→moveend→scan-Loop aus, 'pausenlos')
- _initMapGL: touchZoomRotate.disableRotation() + touchPitch.disable() + container touch-action:none
  → Pinch=reines Zoom, bleibt in der Karte (kein iOS-Page-Zoom), keine ungewollte Drehung
2026-06-05 10:26:35 +02:00
542106e77b MapLibre-Port Runde 3a: MapGLMarkers in map.js verdrahtet (flag-gated)
- _loadOsmLayers/_loadAll/_addPlaces/_addPoison/_addBreeders/_applyVisibility/_deleteUserPoi
  engine-neutral (GL: POI-DATEN statt Marker, MapGLMarkers.setLayer/setVisible)
- Standort-Dot, Place-Picker-Temp-Marker, Such-Marker als maplibregl.Marker
- engine-spez. Aufrufe über Facade (_mapFlyTo/_mapSetView/_mapResize)
- Wetter + GPS-Recording für GL vorerst gegated (Port in 3b/3c)
- Flag 'by_map_gl'/?mapgl=1, default AUS
2026-06-05 10:06:54 +02:00
63c9be68c6 MapLibre-Port Runde 1: Engine-Fundament (flag-gated, Default Leaflet)
map.js: _useGL()/loadMapLibre()/_initMapGL() + engine-neutrale Facade
(_mapFlyTo/_mapSetView/_mapGetZoom/_mapResize/_mapGetCenter/_mapPaddedBounds, kapselt
[lat,lon]↔[lng,lat]). init() verzweigt auf GL bei Flag 'by_map_gl'/?mapgl=1. Basemap+
Controls+Dark(setStyle)+Scan-Wiring+Crosshair. POI-Layer/Marker = Runde 2. Flag default AUS.
2026-06-05 09:37:59 +02:00
1d64dc5d70 Fix: zentrale Karte nutzte window.UI (undefined) statt bare UI → immer Raster-Branch
ui.js exponiert 'const UI' = globales lexikalisches Binding (bare UI), NICHT window.UI.
map.js _addBasemap prüfte window.UI → immer falsy → Vektor-Pfad nie genommen.
Jetzt: typeof UI-Check. (window.UI?.toast in boot/api/app sind separat tote No-Ops.)
2026-06-05 09:06:09 +02:00
647aa684db Vektor-Basemap: zentrale Karte (pages/map.js) integrieren — sie umging UI.map.create
- map.js _addBasemap: Vektor-Layer (Flag) mit Raster-Fallback, eigener Basemap-Code
- Theme-Wechsel baut Vektor-Layer mit passendem Flavor neu (kein CSS-Filter bei Vektor)
- ui.js: UI.map.vectorEnabled()/vectorLayer() exponiert für Karten mit eigenem Layer-Mgmt
- APP_VER bump
2026-06-05 08:28:11 +02:00
258ccf84ee Feature: Alter fließt in Kalorienbedarf ein (Lebensphasen-Faktoren)
Welpe <4 Mon ×3.0, Junghund 4-12 Mon ×2.0 (Wachstum, überschreibt Aktivität),
Senior 7+ ×0.90, Hochbetagt 11+ ×0.85 des Erwachsenen-Faktors. Lebensphase wird
im Ergebnis angezeigt. SW v1172
2026-06-04 18:04:16 +02:00
cca9a9c70f Fix: Ernährungs-Rechner übernimmt Gewicht+Alter aus Hundeprofil
Falsche Feldnamen: dog.gewicht/dog.alter existieren nicht. Korrekt: gewicht_kg,
und Alter aus geburtstag berechnen (_alterJahre). Beide werden jetzt im Kalorien-
Rechner vorbefüllt. SW v1171
2026-06-04 17:59:34 +02:00
0e77c04eee Fix: gespeichertes Futter-Profil beim Öffnen sichtbar (war hinter Berechnung versteckt)
ernaehrung.js: Das Profil (Marke, Portionen, Notizen, Futter-Typ) wird beim Laden
direkt angezeigt, wenn gespeicherte Daten existieren — vorher nur nach Klick auf
'Kalorienbedarf berechnen'. Gespeicherter Tagesbedarf (kcal_tag) wird 1:1 wieder
gezeigt (kein Neu-Rechnen). _berechne → _showResult refaktoriert. SW v1170
2026-06-04 17:53:03 +02:00
3513aeadb0 Fix: Referral-Code überlebt App-Schließen (localStorage statt sessionStorage)
Werben-Zuordnung ging verloren, wenn die geworbene Person den ?ref=-Link öffnete,
die App schloss und sich erst später registrierte (sessionStorage flüchtig, v.a.
iOS-PWA). Jetzt: Code früh in boot.js nach localStorage (vor evtl. SW-Reload, der
die URL ersetzt), 30-Tage-Ablauf, Löschung nach Registrierung. SW v1169

Datenkorrektur separat: nacho_sarah Angie (id=4) als 2. Werbung zugeordnet.
2026-06-04 17:38:14 +02:00
78866206b4 Feature: Routenaufzeichnung übersteht App-Updates (Guard + Persistenz)
Stufe 1 (Guard): Während aktiver Aufzeichnung wird der SW-/Force-Update-Reload
aufgeschoben (window._byRecording → boot.js/_bySwReload + app.js force-update);
nach Stop/Speichern via window._byReloadIfPending() nachgeholt.

Stufe 2 (Persistenz): Track wird gedrosselt nach localStorage (RecStore) gesichert
und beim nächsten Öffnen der Karten-/Routen-Seite als 'Aufzeichnung fortsetzen?'
angeboten (Resume seedet Track+km+Startzeit). Schützt auch bei Crash/OS-Kill/
manuellem Reload. Greift in map.js UND routes.js. SW v1167
2026-06-04 17:13:23 +02:00
c07b1cc01b Fix: restliche CSP-blockierte Inline-Handler — Bild-Fallbacks (globaler data-fb Error-Handler) + Hover-Effekte (CSS-Utilities + data-hover-play)
App ist jetzt vollständig frei von Inline-Event-Handlern (onerror/onmouseenter/etc.).
data-fb Modi: hide/hide-parent/dim-grandparent/sibling/show-el/emoji/initials + data-fb-src.
Hover: .by-hover-lift/-surface2/-surface3 in utilities.css. SW v1165
2026-06-04 16:22:43 +02:00
2ddd8ac350 Fix: alle funktionalen Inline-Event-Handler → addEventListener/Delegation (von CSP-Härtung 65cfa25 app-weit blockiert)
Chat (senden/öffnen/löschen/Foto), Tagebuch-Buch, KI-Berichte, Wiki-Moderation,
Events-Detail, Walks-Lightbox, Routen-Foto, Navigations-CTAs (data-page),
Presse-Copy + Züchter-Landing (externes JS). 35x UI.modal.close → data-modal-close,
28x totes event.stopPropagation entfernt. Verbleibend: kosmetische onerror/Hover. SW v1164
2026-06-04 13:59:27 +02:00
152fde716c Fix: Freunde Annehmen/Ablehnen/Chat per Event-Delegation statt Inline-onclick (von CSP-Härtung 65cfa25 blockiert), SW v1163 2026-06-04 13:35:53 +02:00
55b354e865 Freunde: Annehmen/Ablehnen-Buttons mit Text-Label (Icon-only war für Nutzer nicht erkennbar) + Fix /apifriends/same-breed Slash-Bug, SW v1162 2026-06-04 12:27:01 +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
91624dac25 Fix: gelaufene km bei Routen-Navigation gehen verloren wenn nicht über
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.
2026-06-04 09:09:07 +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
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
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
f934560a12 Routenübersicht: immer Karten-Preview statt erstem Foto, Kamera-Badge bei Routen mit Bildern
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.
2026-05-30 18:16:46 +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
6bc63e3818 dog-profile: 'Später erinnern'-Button auf Standalone-Anlage-Seite
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.
2026-05-30 17:38:12 +02:00
fb9620fbcb Onboarding: 'Los geht's' navigiert direkt zum vollen Hunde-Profil-Formular statt Mini-Wizard. Welcome-'Hund anlegen' nutzt echten Event-Listener statt inline onclick. 2026-05-30 17:19:11 +02:00
2d43618dc8 Onboarding-Wizard: kein Stuck-State mehr bei vorhandenem Hund 2026-05-30 17:07:24 +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
b239eee0d6 Wetter: aktuelle Ist-Temperatur als Jetzt-Banner oben (API.weather.get parallel), SW v1121 2026-05-29 08:36:30 +02:00
c7a84438d1 Fix: Notes-Karten — Zeilenumbruch, Clamp + Detail-Modal beim Klick, SW by-v1119
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)
2026-05-27 14:42:47 +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
c517c9281d Refactor: 1167 _esc() → UI.escape() in 36 Dateien, SW by-v1113
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.
2026-05-27 10:15:33 +02:00
2d98eb9374 Fix: Friends-Avatare wieder Original-URL (kein Preview), SW by-v1111
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.
2026-05-27 09:25:19 +02:00
2f37e0ed16 Perf: Freunde-Seite nutzt _preview.webp + lazy loading, SW by-v1109
Symptom: Friends-Seite lädt Avatare langsam — Original-Bilder
(z.B. 4-12MB iPhone-Fotos) statt der vorhandenen _preview.webp
Vorschauen.

Neue zentrale Helper in ui.js:
- UI.previewUrl(url): ersetzt /media/...jpg → /media/..._preview.webp
- UI.previewFallback(originalUrl): onerror-Handler der Original
  nachlädt falls _preview nicht existiert (für ältere Uploads)

friends.js 3 Stellen migriert:
- _userAvatar (Freundes-Karte + Aktivitäts-Feed)
- Activity-Avatar (dog_foto + avatar_url)
- Dog-Mini-Thumbs im Profil-Modal

Zusätzlich auf allen drei Stellen:
- loading="lazy" für off-screen Bilder
- decoding="async" damit der Hauptthread nicht blockiert

Reuse-Potential: wiki.js, dog-profile.js und andere können später
auf die zentralen Helper umgestellt werden.
2026-05-27 08:50:09 +02:00
f6633d65b0 Erste-Hilfe: Telefonnummern für AT + CH eingetragen, SW by-v1108
AT:
- VetMedUni Wien Kleintier-Notdienst (24h): +43 1 25077-6900

CH:
- Tox Info Suisse: 145 (in CH gratis) bzw. international +41 44 251 51 51
  (offizielle Notruf-Nummer auch für Tiergifte)
- Tierspital Zürich Kleintier-Notfall (24h): +41 44 635 83 37

Damit alle TODO-Platzhalter aus der Sprint60-Erweiterung der
Erste-Hilfe-Notfallnummern jetzt mit echten Nummern befüllt.
2026-05-27 08:45:43 +02:00
73872e2c21 Sprint D: Karten-Familie auf UI.map.create+svgMarker konsolidiert, SW by-v1107
Neue zentrale Helper (in Sprint B vorbereitet) jetzt von 5 Seiten genutzt:

walks.js (1 Karten-Init):
- L.map+L.tileLayer → await UI.map.create('walks-map', {...})
- _initMap zu async, Aufrufer in _switchView und _loadData angepasst
- Mini-Karte im Walk-Formular (Modal) bleibt unverändert
  (braucht eigene dragging/scrollWheelZoom-Options)
- view-toggle nicht migriert (responsive CSS-Konflikt mit Desktop)

poison.js (1 Karten-Init):
- L.map+L.tileLayer → await UI.map.create('poison-map', {...})
- _initMap zu async, manueller UI.loadLeaflet entfernt
- DangerCircle + User-Marker unverändert

events.js (1 Karten-Init + Diamant-Marker):
- await UI.map.create('ev-map', {...})
- Rotierter Diamant: L.divIcon+L.marker → UI.map.svgMarker
  (HTML 1:1 erhalten)

lost.js (1 Karten-Init + Puls-Marker):
- Eigene async _loadLeaflet() Funktion komplett entfernt — UI.map.create
  übernimmt das jetzt zentral
- await UI.map.create('lost-map', {...})
- Puls-Animation 🐕: L.divIcon+L.marker → UI.map.svgMarker
- _initMap zu async

routes.js (6 von 7 Karten-Inits):
- _suggestMap, _recMap, _searchMap, _navMap, trimMap, _buildDetailMap
  alle auf UI.map.create umgestellt + zu async
- _buildMiniMap (Route-Card-Preview) bleibt unverändert
  (braucht 6 spezifische Interaction-Disable Options)
- View-Toggle auf neue .map-list-toggle Klasse umgestellt
  (Border-Inline-Styles raus)

NEUE CSS-KLASSE in components.css:
- .map-list-toggle (vereinheitlichter Karten/Listen-Umschalter)
- Verwendet von routes.js; walks/events können später folgen

Tests 19/19 grün. GPS-Tracking-Logik (Polylines, Recording, Trim)
komplett unangetastet. Marker-Cluster-Logik unverändert.
2026-05-27 08:17:06 +02:00
9a066cb24c Sprint C: Listen-Familie konsolidiert (Notes/Expenses/Health), SW by-v1104
Neue zentrale CSS-Datei lists.css (~280 Zeilen) mit Listen-Komponenten:
- .list-shell, .list-filter-bar, .list-search-wrap
- .list-group-header
- .list-item-card + Modifier: --clickable, --milestone, --inactive
- .list-item-date-col + sub-elements (für Diary-Style)
- .list-item-meta-badge mit --meta-color (für Expenses/Health Icons)
- .list-item-body, .list-item-title, .list-item-text, .list-item-meta-row
- .list-item-chips + .list-item-chip mit --chip-color
- .list-item-micro-badges + .list-item-micro-badge
- .list-item-thumb (+ .list-item-thumb-count Overlay)
- .list-item-amount (+ --positive/--negative/--neutral)
- .list-item-actions + .list-item-action-btn (+ --danger)
- .list-reminders-banner + .list-reminder-item (+ --urgent/--warning/--success)
- .list-fab (FAB mit safe-area-inset)

MIGRATIONEN:

notes.js — 10+ Klassen ersetzt:
- .notes-card → .list-item-card list-item-card--clickable
- .notes-rubrik-chip → .list-item-chip mit --chip-color
- .notes-card-meta → .list-item-meta-row
- .notes-action-btn → .list-item-action-btn
- .notes-group-label → .list-group-header
- Notes-spezifische Klassen als Modifier behalten (vertikales Layout,
  pre-wrap text, Top-Zeile mit Actions rechts oben)
- Alte CSS-Definitionen im Inline-<style> als TODO markiert

expenses.js — komplette Item-Card-Migration:
- .exp-entry → .list-item-card list-item-card--clickable
- .exp-entry-icon-badge mit --kat-color → .list-item-meta-badge --meta-color
- .exp-entry-betrag → .list-item-amount list-item-amount--negative
- .exp-entry-del → .list-item-action-btn list-item-action-btn--danger
- .exp-recurring-card--inaktiv → .list-item-card--inactive
- .exp-fab → .list-fab
- UI.moneyInput + UI.parseMoney in beide Forms integriert (€-Prefix,
  Komma-Dezimal)
- Hero-Card + Statistik/Kacheln behalten (spezifisch)

health.js — 9 Card-Renderings migriert:
- Impfungen/Tierarzt/Gewicht/Läufigkeit/Medikamente/Allergien/
  Dokumente/Tierarztpraxis/Befunde
- .health-card → .list-item-card list-item-card--clickable
- Health-Ampel parallel behalten (.health-card-ampel + Linie links)
- Reminder-Banner: .health-reminder-* → .list-reminders-banner +
  .list-reminder-item--urgent/--warning
- Gewicht-Wert: .list-item-amount für kg-Anzeigen
- Form-Modals + KI-Buttons + Transponder-Chip unangetastet (anderer
  Scope)

Tests 19/19 grün. Kein visueller Diff erwartet — Modifier-Klassen
bewahren spezifische Layouts.
2026-05-27 07:31:21 +02:00
459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +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