Commit graph

938 commits

Author SHA1 Message Date
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
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
6ad7c4be77 Text: Rassen-Wiki Vergleichstabelle — '> 1.000' statt '1.003 (KI-angereichert)', SW by-v1097 2026-05-26 20:51:46 +02:00
c785becd95 Fix: VERSION-Datei ins Docker-Image kopieren, SW by-v1096
main.py liest APP_VER aus /app/VERSION beim Container-Start.
Dockerfile kopierte aber nur backend/, nicht die VERSION im Root.
→ /api/version lieferte '0'.

Fix: explizit COPY VERSION /app/VERSION ergänzt.
2026-05-26 20:13:35 +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
6224044654 Fix: Long-Press auf FAB 350ms statt 600ms + iOS-Textauswahl unterdrücken, SW by-v1094
User-Feedback: 600ms Long-Press triggert iOS-Textauswahl.

- Trigger-Dauer von 600ms auf 350ms verkürzt — feuert vor iOS-
  Auswahl-Callout (typisch ~500ms)
- Am #worlds-fab user-select:none, -webkit-user-select:none,
  -webkit-touch-callout:none — verhindert dass iOS bei Press ein
  Lupensymbol/Auswahl-Menü öffnet
2026-05-26 19:18:38 +02:00
61af803d99 UX: Step 4 wieder strikt + Long-Press auf FAB = Status-Modal, SW by-v1093
- Step 4 wieder strikt: alle 3 (expenses + routes + notes) müssen
  im Cache sein, sonst weiß. 5/5 Braun = wirklich alles bereit.
- Long-Press (600ms) auf #worlds-fab öffnet das Status-Modal mit
  detaillierter 5-Punkte-Checkliste + 'Fehlende nachladen'-Button.
  Normaler Click bleibt unverändert (FAB-Schnellaktion).
  Wenn Long-Press feuert, wird nachfolgender Click unterdrückt.
- _bindLongPress wird mehrfach gebunden falls FAB neu gerendert
  wird (data-lpBound verhindert Doppel-Binding).
2026-05-26 19:13:12 +02:00
66d2d96a2f Fix: Step 4 toleranter + Prefetch im 60s-Interval, SW by-v1092
Step 4 verlangte alle 3 von expenses/routes/notes — einer fehlte
(vermutlich notes 401 weil beim Prefetch noch nicht eingeloggt) →
Pfote weiß. Jetzt reichen 2 von 3.

Außerdem läuft _prefetchData() nicht nur beim Init + 2/5/10/20s-
Retries, sondern auch alle 60s mit refresh() — falls Login erst
spät erfolgt, kommt die fehlende API beim nächsten Tick.
2026-05-26 18:22:20 +02:00
0ba0de12b3 UX: Wetter-Preset + robusteres Tile-Prefetch (gemeinsamer LastPos), SW by-v1091
Beide nutzen jetzt by_last_position aus localStorage:

- wetter.js: speichert jede erfolgreiche GPS-Position; bei GPS-Fehler
  (kein Netz, Permission verweigert) wird der letzte bekannte Ort
  als Fallback genutzt — kein Error-Banner mehr wenn man ihn schon
  einmal hatte
- offline-indicator.js: _prefetchTiles versucht erst GPS, fällt
  dann auf den gespeicherten letzten Ort zurück → Step 5 wird auch
  ohne aktive GPS-Permission grün, sobald Wetter (oder andere
  Module) einmal eine Position eingeloggt haben
- TILE_MIN von 50 auf 20 gesenkt — 5x4 Tiles reichen für eine
  brauchbare Offline-Karte im Nahbereich
2026-05-26 17:10:52 +02:00
2876469e91 UX: Offline-Pfote in Ban-Yaro-Braun statt Grün, SW by-v1090
Filled-Farbe der Pfoten-Linien von #16a34a (grün) auf #5C3517
(dunkles Ban Yaro Braun) — passt zum Brand statt fremder
Signalfarbe, klar erkennbar auf orangem FAB.
2026-05-26 16:00:25 +02:00
d47fb61abf Fix: /api/notes ins SW Cacheable-Liste aufnehmen, SW by-v1089
Step 4 der Offline-Pfote (Weitere Listen) blieb weiß weil
/api/notes nicht in _CACHEABLE_GET stand — Notes-Requests wurden
vom SW gar nicht gecacht, egal wie oft sie geladen wurden.

Regex /^\/api\/notes/ ergänzt — jetzt cached der SW Notes-GETs
mit Stale-While-Revalidate (default 5min TTL).
2026-05-26 15:56:03 +02:00
03725d6682 Fix: Offline-Pfote — Step 2+3 tolerant, mehr Prefetch, SW by-v1088
Steps wurden nicht grün weil Probes zu strikt waren (alle 7 Module
bzw. 3 URL-Patterns erforderlich) — Cache-Inhalt zum Refresh-
Zeitpunkt oft unvollständig.

- Step 2 toleriert 1 fehlendes Page-Modul (have >= want-1)
- Step 3 verlangt nur noch Profil ODER welcome-dashboard PLUS Diary
  ODER Health (nicht beides)
- Neuer _prefetchPages() lädt alle 10 Page-Module proaktiv beim
  App-Start — unabhängig von SW-Install-Status
- _prefetchData() wird jetzt mehrmals retried (2s, 5s, 10s, 20s),
  damit hund-spezifische Daten geholt werden sobald
  _appState.activeDog gesetzt ist
2026-05-26 15:52:20 +02:00
87462cb2fe UX: Offline-Pfote misst echte Offline-Bereitschaft, SW by-v1087
Steps so umverteilt dass sie genau die Datentypen abdecken die der
User offline braucht — auch wenn er die Seiten nie geöffnet hat:

  1 App-Grundgerüst     CSS + Core-JS
  2 Wichtige Seiten     alle 10 Page-Module (precached via SW)
  3 Hund-Daten          Profil + Tagebuch + Gesundheit
  4 Weitere Listen      Ausgaben + Routen + Notizen
  5 Karten-Kacheln      OSM-Tiles im Umkreis

Automatischer Prefetch im _prefetchData() beim App-Start:
- /api/expenses · /api/routes · /api/notes (Step 4)
- /api/dogs/{id}/health · /api/dogs/{id}/diary (Step 3)
- Tiles via _prefetchTiles wenn GPS-Permission da (Step 5)

Wiki, Übungen, Streak, Wetter werden NICHT mehr vorgeladen — kommen
beim normalen Welten-Besuch ins Cache, sind aber nicht Pflicht für
'offline-bereit'.

setTimeout-Retry nach 3s: aktiver Hund ist beim ersten Init oft
noch nicht in _appState, danach kommt der Health/Diary-Prefetch.
2026-05-26 15:34:42 +02:00
307b4a5486 UX: Offline-Pfote — automatischer Tile-Prefetch + Step 5 umgebaut, SW by-v1086
- Step 5 misst jetzt 'Welt-Daten' (Streak + Wetter) statt
  Wiki/Übungen — die kommen automatisch beim Welten-Aufruf, kein
  zusätzliches Prefetch nötig (User-Wunsch: Wiki+Übungen NICHT
  preloaden)
- Neuer _prefetchTiles(): beim App-Start werden 49+9 OSM-Tiles
  (Zoom 14 +13) im 3km-Umkreis automatisch gecacht — aber NUR wenn
  GPS-Permission schon erteilt ist (kein nerviger Popup beim
  Start). Damit wird Step 4 nach kurzer Zeit grün.
- _fetchMissing für Step 5 lädt jetzt Streak + Wetter (statt Wiki)
2026-05-26 15:25:54 +02:00
94f02dbe3a UX: Mehr Offline-Seiten precachen + nur Strich grün, SW by-v1085
PRIORITY_PAGES erweitert auf 10 Seiten (war 8): zusätzlich
health.js, notes.js, expenses.js. admin.js raus — 233 KB, offline
irrelevant. Damit funktionieren offline ohne vorherigen Besuch:
Tagebuch · Gesundheit · Karte · Gassi · Erste Hilfe · Notizblock
Ausgaben · Routen · Giftköder · Vermisst.

Offline-Indikator Step 2 prüft jetzt alle 7 vom User genannten
Seiten (diary, map, walks, erste-hilfe, notes, expenses, routes) —
Pfote wird grün wenn alle im Static-Cache sind.

CSS-Färbung umgestellt: nur stroke (Linie) wird grün, kein fill
mehr. Pfote behält ihre offene Optik, nur die Outlines wechseln
von weiß zu Grün.
2026-05-26 15:14:07 +02:00
95dccd03be Fix: Offline-Score Cache-Detection robust, SW by-v1084
Bug: APP_VER war in app.js nur lokale const, nicht window.APP_VER
→ offline-indicator.js öffnete Cache 'by-v0-static' statt
'by-v1083-static' → fast alle Stufen blieben grau.

Fixes:
- app.js: window.APP_VER + window.APP_VERSION explizit setzen
- offline-indicator.js: _staticCache() Helper findet den aktuellen
  Static-Cache per Regex /^by-v\d+-static$/ — versions-unabhängig
- Step 1 (App-Shell) prüft jetzt korrekt auf design-system.css UND
  app.js im Static-Cache, nicht mehr caches.match() mit URL
2026-05-26 15:06:43 +02:00
b9fe5b5bc3 UX: Offline-Score direkt im FAB statt separater Pfote, SW by-v1083
User-Feedback: separater Indikator zu viel — die Pfote IM FAB selbst
soll je nach Score grün eingefärbt werden.

- Separater #offline-indicator Button entfernt (HTML + CSS)
- Welten-FAB-Icon: <use phosphor.svg#paw-print> ersetzt durch
  Inline-SVG mit 5 einzelnen paw-elem-Pfaden (1 Ballen + 4 Zehen)
- CSS: Default weiß (wie bisher), .filled wird leuchtendes Grün
  (#16a34a) — überzeichnet auf orangem FAB klar erkennbar
- offline-indicator.js: zeigt jetzt nur noch die FAB-Pfade ein/aus,
  kein eigenes Element mehr; Klick-Status-Modal als window.OfflineIndicator.openStatus() weiter verfügbar (kann
  später bei Bedarf an Long-Press oder Menüpunkt gehängt werden)
2026-05-26 14:57:19 +02:00
53c80b9bf6 Fix: Offline-Pfote sichtbar by-default, JS versteckt nur, SW by-v1082
Logik umgedreht: Default ist 'sichtbar', JS setzt .is-hidden nur wenn
explizit nicht in Welten. So robust gegen Sibling-Selektor-Probleme
oder CSS-Compositing-Eigenheiten auf iOS PWA.

Außerdem: Hintergrund prominenter (rgba 0.95 statt 0.85), echter
Border statt Glas-Filter, stärkerer Schatten — bei den vorigen
Versuchen war die Pfote vermutlich auch durch Transparenz schwer zu
erkennen auf grauem Hintergrund.
2026-05-26 14:43:56 +02:00
eb0f460304 Fix: Offline-Pfote per JS-Klasse sichtbar (Fallback zum CSS-Sibling), SW by-v1081
Der reine CSS-Sibling-Selektor klappte nicht zuverlässig (vermutlich
SW-Cache-Mismatch oder DOM-Reihenfolge im aktuellen Zustand des
Users). Lösung: MutationObserver in offline-indicator.js beobachtet
class/style auf #worlds-overlay und togglet .visible auf
#offline-indicator. CSS akzeptiert jetzt beide Wege:

  #worlds-overlay.worlds-visible ~ #offline-indicator,
  #offline-indicator.visible { display: flex; }

So bleibt das Layout funktional auch wenn CSS-Compositing oder
Cache-Versatz mal nicht greift. console.warn wenn das Element nicht
im DOM ist (z.B. wenn alte index.html aus SW-Cache).
2026-05-26 14:36:27 +02:00
521b7b6bee UX: Offline-Pfote über FAB + nur in Welten sichtbar, SW by-v1080
- Position: bottom-right über dem #worlds-fab (right:20px, bottom-
  Berechnung folgt FAB + 12px Abstand). Gleiche horizontale Achse
  wie FAB → ergibt eine 'Pfoten-Säule' (Indikator oben, FAB unten)
- Sichtbarkeit per CSS-Sibling-Selektor:
  #worlds-overlay.worlds-visible ~ #offline-indicator { display:flex }
  → Indikator nur sichtbar wenn Welten aktiv sind. Auf Detail-Seiten
  (Tagebuch, Karte, Admin etc.) bleibt er aus.
- z-index 61 (eine Stufe über dem FAB, unter Modals)
2026-05-26 14:30:57 +02:00
06b91dc54b Fix: Offline-Pfote als schwebendes Element (Welten verstecken Header), SW by-v1079
Der Header (#app-header) ist in den Welten per 'display:none !important'
ausgeblendet (Welten übernehmen Navigation). Mein Pfötchen saß da
drin und war genau dort unsichtbar wo es sichtbar sein sollte.

- Button aus dem Header rausgeholt, am Ende vom body als schwebendes
  Element platziert (position:fixed; top-right; z-index:9000)
- Eigener Stil: 40px runder Glas-Hintergrund, blur-Effekt, leichter
  Schatten — passt zur FAB-Optik unten rechts
- Dark-Mode Hintergrund: dunkles Glas
- Sichtbar in allen Welten und auf allen Seiten (auch wo Header da
  ist — sitzt daneben)
- 'hidden'-Default raus, Element ist sofort sichtbar (nur Färbung
  wartet auf refresh())
2026-05-26 14:24:45 +02:00
776641fa65 Fix: Offline-Indicator Cache-Namen + Step-5-Check, SW by-v1078
- CACHE_API hieß bei mir 'by-api', tatsächlich aber 'ban-yaro-api-v1'
  → korrigiert, sonst hätte step 3+5 nie grün werden können
- Step 5 prüfte auf gecachte Diary-Foto-Previews — die werden vom SW
  aber gar nicht gecacht (nur API-Routen sind in _CACHEABLE_GET).
  Stattdessen jetzt 'Training & Wissen' (training/exercises +
  wiki/rassen) — ist im SW-Cache abgedeckt und passt zur WELT-Welt
- _fetchMissing für Step 5 entsprechend angepasst
2026-05-26 14:18:47 +02:00
8097d21605 Feature: Offline-Bereitschafts-Indikator (Pfote im Header), SW by-v1077
- Neue Pfote oben rechts im Header zeigt 5-stufige Offline-Bereitschaft
  (1 Ballen + 4 Zehen, je 20% — grau outline → grün gefüllt)
- 5 Checks: App-Shell · Page-Module · Hund-/Tagebuchdaten · Karten-
  Tiles (≥50) · Foto-Previews
- Klick öffnet Status-Modal mit Checkliste + 'Fehlende nachladen'-
  Button. Lädt aktiv: Page-Module per fetch, API-Daten für aktiven
  Hund, Tile-Cache per SW-Message CACHE_TILES, Diary-Foto-Previews
- Refresh: alle 60s + bei SW CACHE_UPDATE-Message
- Eigene offline-indicator.js (nicht im app.js mit reingequetscht);
  ins STATIC_ASSETS-Precache aufgenommen
2026-05-26 14:16:57 +02:00
280213c11d UX: Rechnungs-Modal Footer für Mobile, SW by-v1076
Footer-Layout neu strukturiert — kein Umbruch-Chaos mehr:
- Erste Zeile: Abbrechen | Speichern (Grid 1fr 1fr, gleich breit)
  oder bei sent/paid nur 'Schließen' volle Breite
- Zweite Zeile (wenn vorhanden): Stornieren als volle Breite,
  ghost-Style mit rotem Rand — destruktive Aktion klar getrennt
- Button-Text 'Änderungen speichern' → 'Speichern' (kein Abschneiden
  mehr auf iPhone)
2026-05-26 14:03:09 +02:00
c4a82e96fd UX: Rechnungs-Edit-Modal status-bewusst + Storno-Button, SW by-v1075
- _openNeueRechnungModal akzeptiert jetzt status+invoice_number; je
  nach Status anderes Verhalten:
  • draft   → bearbeitbar, Stornieren + Abbrechen + Speichern
  • sent    → readonly Banner, Stornieren + Schließen
  • paid    → readonly Banner, Stornieren + Schließen
  • neu     → Abbrechen + Erstellen (kein Storno-Slot)
- Footer-Layout: Stornieren links, Abbrechen/Speichern rechts
  (justify-content:space-between → symmetrisch)
- Stornieren öffnet den existierenden _openStornoModal; das Edit-
  Modal wird durch UI.modal.open() automatisch geschlossen
- Submit-Handler ignoriert locked-Status (Schutz auch wenn Button
  irgendwie sichtbar wäre)
- Upgrades-Tab + Rechnungen-Tab geben jetzt inv.status+invoice_number
  beim Öffnen mit; Reload-Callback aus Upgrades-Tab rendert den Tab
  neu, damit nach Stornierung der gelbe Button zurück auf orange geht
2026-05-26 13:59:40 +02:00
5886e1b269 UX: Upgrades-Tab — Button zeigt vorhandene Rechnung an, SW by-v1074
- Backend: /admin/upgrade-requests liefert pro Request die offene
  Rechnung (id+number+status) per Subquery aus der invoices-Tabelle
  (status draft|sent → also nicht bezahlt, nicht storniert)
- Frontend: Wenn schon eine Rechnung existiert, wird statt 'Rechnung
  erstellen' (orange) der Button 'Rechnung bearbeiten' (gelb,
  #eab308) gezeigt. Klick lädt die Rechnung und öffnet das Modal im
  Edit-Modus — kein doppeltes Anlegen, Nummerierung bleibt sauber.
2026-05-26 13:50:03 +02:00
e5abdcab62 Fix: Tagebuch Foto-Löschen — null-crash + 404-Cleanup, SW by-v1073
- 'null is not an object (wrap2.remove)': Wrapper-Div hat keine
  Klasse .diary-media-thumb-wrap → closest() lieferte null. Fallback
  auf btn.parentElement + Null-Check vor remove()
- Bei 404 'Medium nicht gefunden' wird das verwaiste Foto jetzt
  trotzdem lokal aufgeräumt (entry.media_items + DOM), statt einen
  Error-Toast zu zeigen. Verwaiste Phantome verschwinden so beim
  ersten Lösch-Klick.
2026-05-26 13:38:11 +02:00
c03884cb81 Perf: 9 Performance-Fixes — SW by-v1072
Backend:
- DB: 3 neue Indizes (forum_posts thread+user, routes user) — Forum/Routen-Queries
- Caching: cache.py (TTL-Cache ohne neue Dependency) für 5 statische Listen
  (training_exercises, pflege_tipps, wiki_stats, wiki_gruppen, help_articles)
- diary.py + breeder_photos.py: Bildverarbeitung (ffmpeg/PIL/EXIF) per
  run_in_executor → blockiert Event-Loop nicht mehr
- scheduler.py: 11 kollidierende Jobs auf 5-Min-Intervalle gestaggert, coalesce=True
- social.py: ORDER BY RANDOM() ohne LIMIT in 2 Stellen gefixt
- alerts.py: Haversine-Loop bekommt SQL-Bounding-Box-Vorfilter

Frontend:
- sw.js: Tile-Cache mit LRU-Eviction (max 500 Einträge)
- admin.js: Event-Listener-Leak — Tab-Klicks per Delegation statt N Listener
- api.js: compressImage() Helper — Client-seitiges Resize auf max 2000px
  (HEIC/Videos/<500KB unverändert), integriert in 8 Upload-Stellen
  (diary, dog-profile×2, walks, poison, lost, health×2)

Bump APP_VER 1071 → 1072 (sw.js, app.js, main.py, index.html)
2026-05-26 06:30:36 +02:00
3abf974d29 Feature: Parallele Bild-Uploads, Heartbeat last_seen, Admin zuletzt aktiv, SW by-v1071
- Tagebuch: Bilder werden parallel hochgeladen (Promise.all), Button zeigt Fortschritt
- Auth: /heartbeat Route ergänzt — aktualisiert last_seen alle 5 Min
- Admin: last_seen + last_login in Nutzer-Liste angezeigt (🟢/🔵/)
- Bump SW by-v1071
2026-05-25 20:26:58 +02:00
9677d1e71a Fix: Pfoten-Button bleibt rechts (display:flex statt '' beim Einblenden), SW by-v1070 2026-05-19 19:04:32 +02:00
9f47cfe1d4 Fix: Pfoten-Button rechts neben Suchzeile (Dog-Chip eigene Zeile), SW by-v1069 2026-05-19 19:02:22 +02:00
b1e4d7fce1 Bump: SW by-v1068 (Übungen-Redesign) 2026-05-19 18:58:12 +02:00
c5bb3ee2cb UX: 'Stand erfassen' als Pfoten-Button rechts neben Dog-Chip+Suche 2026-05-19 18:51:17 +02:00