Container startete mit USER appuser nicht: SQLite gibt
'attempt to write a readonly database' — Synology DSM Volume-
Permissions blockieren chown auf gemountete Pfade.
User-Anlage (groupadd/useradd) bleibt im Dockerfile, plus
chown nach mkdir. Nur die USER-Zeile ist auskommentiert mit
Kommentar warum. Für Non-DS-Deployments einfach Zeile
aktivieren.
VAPID-Keys-Migration bleibt — die war erfolgreich.
1. VAPID-Keys aus docker-compose.yml und docker-compose.staging.yml
entfernt. Werden jetzt aus .env gelesen (env_file war schon da,
nur die environment-Override hat die .env-Werte überschrieben).
.env auf DS um die 3 Keys ergänzt:
- VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_CONTACT
Erst Compose-Änderung wirksam — Push-Notifications funktionieren
weiter weil die .env die Werte liefert.
2. Dockerfile-Hardening: Non-root USER appuser.
- groupadd/useradd appuser (UID/GID 1000 für DS-Kompatibilität)
- chown -R appuser:appuser /app /data nach mkdir
- USER appuser vor CMD
Memory says DSM ACLs könnten Probleme machen. Falls Container
nicht startet → Rückbau. Bei Deploy genau hinsehen.
3. E-Mail-Änderungs-Audit-Punkt: kein Vulnerability gefunden.
ProfileUpdate-Schema enthält kein 'email'-Feld. User können
ihre E-Mail-Adresse aktuell gar nicht ändern → kein Takeover-
Vektor wie im Audit vermutet.
NEUE HELPER in auth.py:
require_moderator(user=Depends(get_current_user))
Konsequente Dependency statt inline
'if user["rolle"] not in ("admin", "moderator")'
require_breeder(user=Depends(get_current_user))
Konsequente Dependency statt inline
'if user["subscription_tier"] not in ("breeder", "breeder_test")'
require_owner(row, user, owner_field='user_id',
not_found_msg, forbidden_msg) -> row
Zentralisiert das häufigste Pattern (54 Stellen im Audit):
Statt:
row = conn.execute(...).fetchone()
if not row: raise HTTPException(404, ...)
if row['user_id'] != user['id']: raise HTTPException(403, ...)
Jetzt:
row = require_owner(conn.execute(...).fetchone(), user,
not_found_msg='Ort nicht gefunden.')
is_owner_or_admin(row, user, owner_field='user_id') -> bool
True wenn Owner ODER Admin/Moderator (Admin-Override für
Moderations-Endpoints)
DEMO-MIGRATION:
places.py PATCH /places/{id} + DELETE /places/{id} migriert auf
require_owner() — als Style-Referenz für künftige Migrationen.
KEINE Massen-Migration der 54 Stellen — bewusste Entscheidung
weil security-kritisch. Helper sind bereitgestellt, neuer Code
nutzt sie, bestehender bleibt funktional identisch.
Tests 19/19 grün.
Hinweis: Massen-Migration der Owner-Checks ist eigener Sprint mit
sehr sorgfältigem Testing — bei jeder migrierten Route muss die
404→403→Cascade durchgeprüft werden, dass Owner+Non-Owner+Admin
sich identisch zum Vorher verhalten.
Bündel 1 aus dem Duplikat-Audit: existierende zentrale Helper nutzen
statt lokale Duplikate.
Pure Migration ohne neuen Code:
- 1167 _esc()-Aufrufe in 36 Page-Modulen migriert auf UI.escape()
- 24 lokale _esc/_escape-Definitionen entfernt
- lost.js hatte _escape() (Variante) — 17 Aufrufe ebenfalls migriert
- jobs.js + breeder.js: tote Alias-Wrapper entfernt
UI.escape() existierte schon — wurde nur überall lokal nochmal
implementiert. Funktional identisch (gleiche 4-replace-chain für
& < > ").
Tests 19/19 grün. Frontend-LOC um ~120 Zeilen reduziert.
Hinweis: _emptyState (7 Stellen) und _icon (8 Stellen) wurden NICHT
migriert — sie haben abweichende Signaturen von UI.emptyState({...})
bzw. UI.icon(name). Eigener Sprint nötig.
A — Founder-Number-Race (Audit-Fund aus Agent 2)
- partner.py PATCH /admin/users: SELECT COUNT + UPDATE+1 →
atomares UPDATE mit Sub-Query. WHERE-Klausel prüft Limit + dass
User noch nicht is_founder=1 ist. rowcount=0 → 'Plätze vergeben'.
- dogs.py POST /dogs (erster Hund triggert Gründer-Aktivierung): selbes
Pattern. Zusätzlich AND is_founder_pending=1 als Schutz.
- Sub-Queries werden gegen Snapshot VOR dem UPDATE evaluiert
(SQL-Spec), daher keine 'doppelte Nummer' möglich auch wenn zwei
User gleichzeitig den ersten Hund anlegen.
B — JWT-Blacklist-Cleanup-Job
- _purge_expired_jwt() in auth.py existierte schon, war aber nicht
verdrahtet → jwt_blacklist wuchs monoton.
- Neuer Scheduler-Job _job_purge_jwt_blacklist täglich 03:30
(nach poison_archive, in ruhiger Zeit), mit _log_job für
Error-Digest.
C — iOS Storage-Quota-Watchdog (PWA-Stabilität)
- offline-indicator.js: _checkStorageQuota() per
navigator.storage.estimate() beim Init + alle 60s im Interval.
- Bei >=80% Auslastung: Tile-Cache auf 100 Einträge trimmen (statt
default 500). Verhindert QuotaExceededError auf iOS-PWA (~50MB).
- Bei >=90%: einmaliger Toast-Hinweis pro Session
'Speicher fast voll — Tiles werden gelöscht'.
D — HTTPException in osm.py
- 'raise Exception("Alle Overpass-Instanzen fehlgeschlagen")' wurde
zu HTTP 500 → User-unfriendly. Jetzt 503 mit klarer Message
'Kartendaten gerade nicht verfügbar'.
Tests 19/19 grün.
User-Report: trotz onerror-Fallback weiter Fragezeichen.
Ursache: Das _preview.webp-System wurde damals nur konsequent für
Diary-Uploads ausgerollt. User-Avatare und Hund-Profilbilder haben
keine Preview-Variante → 404 vom _preview triggert kurz das
Browser-Default-Broken-Image-Icon BEVOR der onerror-Fallback das
Original lädt (Race-Condition).
Pragmatischer Fix: Preview-System in friends.js rückgebaut. Bilder
werden direkt mit Original-URL geladen. Performance kommt durch:
- loading=\"lazy\" (off-screen Bilder erst beim Scrollen)
- decoding=\"async\" (Main-Thread bleibt frei)
- onerror=\"this.style.display='none'\" (kaputte Bilder verschwinden
statt Fragezeichen zu zeigen)
UI.previewUrl + UI.previewFallback bleiben als Helper verfügbar
für später falls das Preview-System app-weit ausgerollt wird.
User-Report: nach Sprint-Migration auf _preview.webp tauchen
Fragezeichen-Icons auf — wenn weder Preview noch Original verfügbar.
Probleme im vorigen Fix:
- UI.escape() ist HTML-Escape, kein JS-String-Escape → URL mit
?param=value wurde &-encoded und damit kaputt
- 'opacity:0.3' lässt das Browser-Default-Broken-Image-Icon
durchscheinen (Fragezeichen sichtbar)
- Kein Loop-Schutz beim onerror
Fixes:
- String-Escape via .replace(/'/g, \"\\'\") statt UI.escape()
- display:none + .img-broken-Klasse bei finalem Fehler
- dataset.fb='1' verhindert Endlos-Loop wenn Original-URL auch 404
- Wenn URL nicht mit /media/ startet: direkt ausblenden (keine
Preview-Variante zu probieren)
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.
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.
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)
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.
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.
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
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
- 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"
- <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
- 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
- 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