Commit graph

66 commits

Author SHA1 Message Date
7751d303bb Revert: USER appuser in Dockerfile auskommentiert (DSM-ACL-Konflikt), SW by-v1117
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.
2026-05-27 13:06:25 +02:00
83b1509168 Security: VAPID-Keys raus aus Git, Dockerfile USER appuser, SW by-v1116
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.
2026-05-27 13:02:12 +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
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
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
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
8e75e2b1a7 Fix: previewFallback blendet kaputte Bilder aus statt Fragezeichen, SW by-v1110
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)
2026-05-27 09:20:10 +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
c8ef4939f1 Fix: /force-update reload-Hänger + Cooldown persistent, SW by-v1106
User-Report: 'Leckerlis'-Screen verschwindet nicht mehr.

Bug: Vorige Version nutzte 'await Promise.all([sw-unregister,
caches.delete])' VOR dem Reload. Auf iOS-PWA können diese Promises
gelegentlich nie resolven → Reload kommt nie.

Fix /force-update:
- Cleanup-Tasks fire-and-forget (kein await, kein Promise.all)
- Sofort-Reload nach 150ms (kein await-Block)
- Fallback 1: Nach 3s erscheint 'App neu starten'-Button für
  manuellen Tap
- Fallback 2: Nach 6s automatisch location.href mit ?hard=1

Fix app.js Cooldown:
- localStorage statt sessionStorage — überlebt PWA-App-Close
- 10 Min statt 5 Min Cooldown (großzügiger Spielraum bei
  Update-Wellen)
2026-05-27 08:02:54 +02:00
b0ae71ba69 Fix: Force-Update Cooldown + robusteres Cache-Clear, SW by-v1105
Symptom: 'Einen Moment, wir besorgen neue Leckerlis' Loading-Screen
erscheint beim User wiederholt beim Wechsel in andere Bereiche.

Ursachen:
1. In dieser Session wurden viele Bumps in kurzer Zeit ausgerollt
   (1100 → 1104). Jeder Versions-Mismatch zwischen App-Cache und
   Server triggert force-update.
2. /force-update Cache-Delete war fire-and-forget mit nur 1.5s
   Reload-Timer — auf iOS-PWA oft zu kurz für asynchrone unregister/
   caches.delete, daher landete der Reload manchmal noch im alten
   Cache-Stand → erneuter Mismatch → erneuter force-update.

Fixes:
- app.js: Cooldown 5 Min nach force-update — verhindert Loop bei
  mehrfachen schnellen Bumps. Mismatch wird erkannt aber nicht mehr
  sofort reagiert.
- /force-update: async/await für SW-Unregister + Cache-Delete bevor
  Reload. Safety-Timeout 4s. Reload-URL mit ?_t= Cache-Bust.
2026-05-27 07:51:36 +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
1de39536af Sprint B: 5 neue UI-Helper für konsistente Patterns, SW by-v1103
Neue zentrale Komponenten in ui.js:

1. UI.errorState({icon, title, message, retry})
   Dedizierte Error-UI statt nur Toast. Mit optionalem Retry-Button
   (asyncButton-integriert). Analog zu UI.emptyState. Behebt
   Inkonsistenzen: Toast vs. ad-hoc HTML vs. Empty-State-Reuse.

2. UI.skeletonList(count)
   Karten-Skeleton für Listen-Loading (Avatar + 2 Zeilen pro Item).
   Erweitert UI.skeleton(lines) — beide bleiben verfügbar.

3. UI.moneyInput({name, value, currency='€', placeholder})
   Euro-Input mit Prefix + locale-Format (Komma als Dezimal-
   trenner). Extrahiert aus expenses.js Best-Practice.
   Plus UI.parseMoney(str) als Parser-Helper.

4. UI.datePicker({name, label, value, min, max, required})
   Standard-Date-Input mit Label, min/max ('today' wird zu ISO-
   Datum konvertiert). Vereinfacht Form-Boilerplate.

5. UI.map.create(containerId, opts)
   Zentraler Leaflet-Init mit OSM-Tiles + Dark-Mode-Filter-Option.
   Konstanten OSM_URL + OSM_MAX_ZOOM zentral. Plus UI.map.svgMarker
   für eigene divIcon-Marker (für events.js Diamant, lost.js Puls).

Alle Helper sind backward-kompatibel — bestehende Patterns funktionieren
weiter. Tests 19/19 grün. Migration der Aufrufer kommt in Sprint C+D.
2026-05-27 07:19:52 +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
279f76714e Fix: Offline+Verify-Banner berücksichtigen safe-area-inset-top, SW by-v1101 2026-05-27 06:27:18 +02:00
65cfa25e59 Security: CSP gehärtet — unsafe-inline + unsafe-eval raus, SW by-v1100
Inline-Scripts extrahiert:
- boot-early.js: Theme + theme-color (synchron im <head>, VOR CSS)
- boot.js: Offline-Banner + Service-Worker-Registration + Update-Flow
- landing-init.js: Dark-mode + Scroll-Animationen + Live-Stats +
  Stay-In-App-Handler + Details-Toggle

Inline onclick-Handler in landing.html:
- 5× sessionStorage.setItem('by_stay_in_app','1') → data-stay-in-app
- 1× Details-Toggle → data-toggle-target + data-toggle-text-open
- JS-Handler in landing-init.js binden die data-Attribute

CSP-Header (main.py):
- script-src: 'unsafe-inline' und 'unsafe-eval' entfernt
- style-src 'unsafe-inline' bleibt (Inline-Styles bleiben für jetzt,
  zu viele Fundstellen)
- Umami bleibt whitelisted

SW STATIC_ASSETS erweitert um boot-early.js + boot.js.
make bump aktualisiert jetzt auch landing.html ?v= Anker.
Tests grün (19/19).
2026-05-27 06:23:47 +02:00
6ad7c4be77 Text: Rassen-Wiki Vergleichstabelle — '> 1.000' statt '1.003 (KI-angereichert)', SW by-v1097 2026-05-26 20:51:46 +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
9a60c160a1 Feature: Läufigkeit-Spotlight in Züchter-Sektion (landing.html)
Zeigt ein realistisches Beispiel (Luna vom Bergwald) mit Deckdaten,
Trächtigkeits-Meilensteinen und Progesteronkurve — im bestehenden
Züchter-Abschnitt, kein neuer Section-Break.
SW by-v1017, APP_VER 1017
2026-05-16 10:23:46 +02:00
2caab31797 Feature: Hero-Stats dynamisch aufsteigend sortiert
Statt fixer Reihenfolge werden alle 5 Werte nach dem API-Fetch
per .sort() aufsteigend geordnet und der Streifen neu aufgebaut.
Damit steht immer die kleinste Zahl links, die größte rechts —
unabhängig davon wie die Zahlen wachsen.
SW by-v1000, APP_VER 1000
2026-05-15 18:40:25 +02:00
64127bf395 Fix: Tagebuch-Einträge im Hero-Stats-Streifen (stat-diary)
Statt in der Stats-Band weiter unten erscheint die Zahl jetzt im
Hero-Streifen direkt unter den CTAs — sichtbar ohne Scrollen.
SW by-v999, APP_VER 999
2026-05-15 18:38:59 +02:00
be9f263e0d Feature: Stats-Band + Tagebuch-Einträge, km alle Routen (public+privat)
- landing.html: neues Stats-Band-Element 'Tagebuch-Einträge' (#big-diary)
  mit Wert aus diary_entries (war bereits im API-Response vorhanden)
- stats.py km-Query: explizit WHERE is_valid=1 (kein is_public-Filter —
  private Routen werden mitgezählt, nur ungültige Aufzeichnungen ausgeschlossen)
- SW by-v998, APP_VER 998
2026-05-15 18:34:03 +02:00
d7f7a7e454 Neu: AGB-Seite + Impressum/Datenschutz aktualisiert (SW by-v985)
- Neue Seite agb.js mit 11 Abschnitten (Laufzeit, Zahlung, Widerruf etc.)
- Datenschutz: 'Abonnement & Kündigung' → 'Zahlungsdaten' (DSGVO-Fokus), DDG-Hinweis ergänzt
- Impressum: ODR-Link entfernt (EU-Plattform eingestellt 2025), Telefon-Pflichthinweis nach §5 DDG, Stand Mai 2026
- AGB-Link in alle Footer (index.html, landing.html, zuechter.html, welcome.js)
- page-section #page-agb in index.html, Route 'agb' in app.js ROUTES
2026-05-15 16:21:04 +02:00
129badf010 Fix: Hero-Stats zeigt vollständigen Text 'Mülleimer für Kotbeutel' (SW by-v965) 2026-05-14 22:37:29 +02:00
cf6e5920ae Fix: APP_VER-Mismatch (Dauer-Aktualisieren-Bug), Mülleimer-Zahl im Hero (SW by-v964) 2026-05-14 22:15:17 +02:00
58046ce0c7 Fix: Kotbeutel-Stationen → Mülleimer für Kotbeutel in Stats-Band (SW by-v963) 2026-05-14 22:07:17 +02:00
07db68aea2 Fix: Geburtstag aller Hunde + Kotbeutel-Stationen in Stats (SW by-v962)
- worlds.js: bdayDog = _dogs.find(...) — Geburtstag gilt für alle Hunde, nicht nur den aktiven
- Banner, KI-Call, "Was hat sich X gewünscht?" nutzen bdayDog.name
- stats.py: kotbeutel-Count aus user_map_pois WHERE type='kotbeutel'
- landing: Stats-Band 5. Kachel "Kotbeutel-Stationen"
2026-05-14 22:00:52 +02:00
be87930e5b Fix: Foto-Strip + Ban-Yaro-Bild mit Inline-Styles (SW by-v961) 2026-05-14 19:55:34 +02:00
6f8644c70f Landing: Ban Yaro persönlich — Foto + Geschichte als Split-Section (SW by-v960) 2026-05-14 19:50:12 +02:00
59ddf047ba Perf: Fotos JPG→WebP, 18 MB → 373 KB (SW by-v959)
cwebp: eric-ward 1200px, chewy 1400px, baptist-standaert 1600px,
foto-strip 700x700px — alle q80-82
2026-05-14 19:42:41 +02:00
2a59b775e2 Fix: Eric-Ward-Split mit Inline-Styles und auto-fit Grid (SW by-v958) 2026-05-14 19:33:21 +02:00
7867507e7d Landing: 7 Unsplash-Fotos mit Fotografen-Credits eingebaut (SW by-v957)
- Eric Ward: emotionaler 50/50-Split nach Stats-Band
- Baptist Standaert: subtiles Hintergrundbild Demo-Section
- Alvan Nee / Nicholas Brownlow / Wade Austin Ellis / Tamas Pap: 4er-Foto-Strip vor Testimonials
- Chewy: Header-Bild Welpenkäufer-Section
- Alle Fotos mit © Name · Unsplash Credit-Overlay
2026-05-14 19:25:14 +02:00
e610613b58 Fix: Dark-Mode CSS-Syntax korrigiert — html.dark + @media getrennt (SW by-v956)
Ungültige Syntax (@media..., html.dark kombiniert) aufgelöst.
Zwei separate valide Blöcke: html.dark{} für JS-Klasse, @media{} als Fallback.
2026-05-14 19:15:46 +02:00
10964c6509 Fix: Dark-Mode JS-Fallback via matchMedia + html.dark Klasse (SW by-v955)
matchMedia-Listener setzt html.dark als Fallback für macOS 26 / Brave
die prefers-color-scheme Media Query nicht korrekt weiterleiten
2026-05-14 19:07:36 +02:00
e548c43010 Fix: Dark-Mode Landing mit color-scheme Meta-Tag und !important-Overrides (SW by-v954)
- <meta name="color-scheme" content="light dark"> ergänzt
- color-scheme: light dark / dark in :root
- Alle Dark-Mode-Regeln auf !important umgestellt um Inline-Styles zu schlagen
- #funktionen, #warum, #vergleich, #preise, #ueber ergänzt
2026-05-14 19:01:37 +02:00
996ee9c97e Fix: Landing Dark-Mode, OS-Icons→Phosphor, force-update-Loop (SW by-v953)
- main.py APP_VER 951→953 behebt Update-Loop auf Desktop
- Dark-Mode: vollständige @media (prefers-color-scheme: dark) Regeln für alle Sections
- Emojis im Verbindung-Block (🏡🔍🐶) durch Phosphor SVG ersetzt
- 🐾 in Testimonials und Footer durch paw-print SVG ersetzt
2026-05-14 18:34:02 +02:00
f9160307bc Landing: emotionaler Hero, Social-Proof-Stats, Testimonial-Slots, Scroll-Animationen (SW by-v952)
- Hero-Headline: "Weil jeder Moment mit ihm zählt." (warm/emotional statt Feature-Liste)
- CTA umbenannt: "Kostenlos starten" statt "Ich bin Hundebesitzer"
- Hero-Stats-Zeile: live Nutzer/Hunde/km-Zähler (nur wenn >0)
- Stats-Band: orangener Balken mit 4 Live-Kennzahlen nach der Zwei-Welten-Section
- Testimonial-Section: 3 Platzhalter-Karten zwischen Features und Züchter-Bereich
- Scroll-Animationen: IntersectionObserver auf alle Cards (fade-up)
- API: /api/stats/public — öffentlicher Endpoint, 5-Min-Cache
2026-05-14 18:23:23 +02:00
7e939cf854 SEO: landing.html canonical /info → / (primäre URL ist die Root) 2026-05-14 17:19:05 +02:00
eaa2e02e88 SEO: llms.txt v1.5.1 + sitemap /zuechter + JSON-LD Pricing (Pro 29€/Züchter 49€)
- llms.txt: Dual-Audience-Positionierung, echte Preise, neue Züchter-Features
  (Warteliste, Läufigkeit, Wurf-Buchstabe/-Name, Privater Header, Profilfotos),
  neue URL /zuechter, SW by-v918, Datum 2026-05-14
- landing.html JSON-LD: 3 Offers (kostenlos/Pro 29€/Züchter 49€), 7 neue featureList-Einträge,
  dateModified 2026-05-14, Beschreibung mit Preisen
- zuechter.html JSON-LD: 2 Offers (49€/39€ Gründer), 5 neue Features, dateModified + softwareVersion
- sitemap.xml: neue statische Datei (Backup-Referenz, dynamic route in main.py)
- main.py sitemap: /zuechter mit priority 0.9 hinzugefügt
2026-05-14 09:43:21 +02:00
c5d4e730d9 Feature: Preise live — Pro 29€/Jahr, Züchter 49€/Jahr (Gründer 39€) auf Landing + Züchter-Seite 2026-05-14 09:31:49 +02:00
3da4a1b6d7 Fix: Züchter Icon — volles Orange #C4843A + weißes Icon für maximalen Kontrast 2026-05-14 09:19:05 +02:00
5f5f3e9271 UX: Züchter-Karte Icon heller — rgba .35 + #f5c07a für besseren Kontrast 2026-05-14 09:09:59 +02:00
0a7bb931b3 UX: Landing-Page für Hundebesitzer + Züchter — Split-Hero, Zwei-Welten-Section, Verzahnungs-Section 2026-05-14 08:36:58 +02:00
c25580ec8e SEO: landing.html v1.5.1 + llms.txt auf Stand Mai 2026 (neue Features, Datenschutz, Versionen) 2026-05-12 18:25:42 +02:00
b31116abf6 Fix: Landing-Page Footer/Nav bereinigt — Wiki+Social raus, kaputte CTAs entfernt (SW by-v754) 2026-05-07 17:09:02 +02:00
1fe878924a Fix: Landing-Page — Sitting kostenlos, Phosphor-Icons, KI-Datenschutz korrekt, Pro-Features ausgeblendet (SW by-v753) 2026-05-07 17:05:56 +02:00
cb46f08f2a Fix: Vergleichstabelle landing.html — nur Hundeo+Dogorama, korrekte Daten aus echtem Research (SW by-v749) 2026-05-07 06:24:00 +02:00
bcc7c27556 Landing: Phosphor Icons statt Emoji, SW by-v733 2026-05-06 18:36:23 +02:00