Commit graph

258 commits

Author SHA1 Message Date
bd9acda084 User-Löschen: Upgrade-Anfragen + Hund-Daten mit aufräumen (Admin + Self-Delete)
- admin.py delete_user: löscht jetzt auch Hund-zentrierte Daten (diary,
  health, training_sessions, training_streaks, expenses), dogs,
  upgrade_requests, push_subscriptions, notifications, forum_posts
  bevor der User-Row weg ist. Vorher: nur DELETE FROM users → Waisen in
  allen FK-Tabellen.
- profile.py delete_account: gleicher Cleanup-Set, vergisst jetzt
  upgrade_requests nicht mehr.
- admin.py Dashboard-Counter 'Zu Erledigen': JOIN users, damit
  verwaiste Anfragen nicht mehr im Header-Badge erscheinen (Liste
  selbst filtert sie schon korrekt via JOIN). Bump 1135→1136.
2026-05-30 17:51:24 +02:00
0685f3b8ef Welcome-Dashboard: Tagesfoto wird serverseitig pro Tag gecacht
Neue Tabelle daily_photo_cache(dog_id, datum, photo_url) hält die Wahl
fest, sobald irgendein Client (PWA oder iOS) das erste Mal heute fragt.
Alle nachfolgenden Aufrufe liefern dieselbe URL — kein Kippen mehr, wenn
zwischendrin neue Fotos hochgeladen werden und die tick%len-Rotation auf
einen anderen Index zeigt. CREATE TABLE IF NOT EXISTS inline, daher kein
Migrations-Eingriff nötig.
2026-05-30 12:55:48 +02:00
9b0c286aa3 Expenses: GET /api/expenses/categories liefert die Kategorien
KATEGORIE_META als Dict mit id → {label, color} (preserved Order).
KATEGORIEN bleibt als Set für Validierung. Neuer Endpunkt liefert
[{id, label, color}, …] als Single Source of Truth für PWA und mobile
Clients — bisher war die Liste in JS und Python dupliziert.

Mobile-Client (banyaro-ios) holt die Liste jetzt dynamisch.
2026-05-30 12:31:59 +02:00
7b3041fc94 DRY: Notiz-Modal zentral in UI.noteModal (11 divergierte Kopien entfernt, ~750 Z. weniger); Fix: Founder-Race in jobs.py atomar + founder_number, SW v1133 2026-05-29 10:51:42 +02:00
a356626d39 Feature: Pflege-Routinen (Zecken-/Flohschutz, Krallen, Fellpflege) — neuer Pflege-Tab mit Erledigt+Auto-Wiedervorlage, Push-Erinnerungen, intervall_tage-Fix im INSERT, SW v1132 2026-05-29 10:32:05 +02:00
26b515cede Fix: Anniversary-Job + RASFF 404, SW by-v1120
Aus Container-Log gefundene Backend-Errors:

1. _job_anniversary_reminders: 'no such column: d.user_id'
   diary-Tabelle hat keine user_id — User-Bezug geht über dogs.user_id.
   → JOIN dogs ON dogs.id = d.dog_id ergänzt + SELECT dogs.user_id.
   Job läuft täglich 09:00 — war seit Tag X kaputt, kein Push für
   Jahrestage gesendet.

2. RASFF API 404 (EU Rapid Alert System for Food and Feed):
   webgate.ec.europa.eu/rasff-window/backend/public/... ist umgezogen.
   → HTTPStatusError mit 404/410/503 wird jetzt nur als WARNING geloggt
   (vorher ERROR → Error-Digest spammte täglich). Fallback ist eh schon
   ein leeres Array, App läuft weiter. EU-Endpoint-URL muss nochmal
   recherchiert werden, dann RASFF_URL aktualisieren — Folge-Sprint.
2026-05-27 14:51:34 +02:00
1ff66a7083 Sicherheit + Tests + A11y, SW by-v1118
PYDANTIC max_length (38 Routen, ~400 Field-Constraints):
Schützt vor DoS durch Riesen-Payloads (10MB Thread-Titel etc.).
Pragmatische Limits:
- Titel/Name: 200 · Beschreibung/Body: 10000 · Notiz: 5000
- Email: 254 (RFC 5321) · URL: 500 · Slug/Kategorie: 100
- Hund-Name/Rasse: 80 · Hund-Bio: 2000

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

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

A11Y (Tap-Targets ≥44×44 + Dark-Mode-Kontrast):
- #header-user-btn 36→44px, .header-back 40→44, .header-menu-btn 40→44
- dog-profile Wrapped-Slider Prev/Next 40→44
- forum-Lightbox Close 40→44
- --c-text-muted Light: #B0A090 (2.37:1 FAIL) → #7F6B58 (4.74:1 PASS)
- --c-text-muted Dark:  #806A58 (3.58:1 FAIL) → #A08878 (5.46:1 PASS)
- Branding-Farben unangetastet
2026-05-27 13:40:30 +02:00
35937ed51b Bündel 3: Security-Helper + Demo-Migration, SW by-v1115
NEUE HELPER in auth.py:

require_moderator(user=Depends(get_current_user))
  Konsequente Dependency statt inline
  'if user["rolle"] not in ("admin", "moderator")'

require_breeder(user=Depends(get_current_user))
  Konsequente Dependency statt inline
  'if user["subscription_tier"] not in ("breeder", "breeder_test")'

require_owner(row, user, owner_field='user_id',
              not_found_msg, forbidden_msg) -> row
  Zentralisiert das häufigste Pattern (54 Stellen im Audit):
  Statt:
    row = conn.execute(...).fetchone()
    if not row: raise HTTPException(404, ...)
    if row['user_id'] != user['id']: raise HTTPException(403, ...)
  Jetzt:
    row = require_owner(conn.execute(...).fetchone(), user,
                        not_found_msg='Ort nicht gefunden.')

is_owner_or_admin(row, user, owner_field='user_id') -> bool
  True wenn Owner ODER Admin/Moderator (Admin-Override für
  Moderations-Endpoints)

DEMO-MIGRATION:
places.py PATCH /places/{id} + DELETE /places/{id} migriert auf
require_owner() — als Style-Referenz für künftige Migrationen.

KEINE Massen-Migration der 54 Stellen — bewusste Entscheidung
weil security-kritisch. Helper sind bereitgestellt, neuer Code
nutzt sie, bestehender bleibt funktional identisch.

Tests 19/19 grün.

Hinweis: Massen-Migration der Owner-Checks ist eigener Sprint mit
sehr sorgfältigem Testing — bei jeder migrierten Route muss die
404→403→Cascade durchgeprüft werden, dass Owner+Non-Owner+Admin
sich identisch zum Vorher verhalten.
2026-05-27 11:27:00 +02:00
297bd22f96 Bündel 2: Zentrale Helper für DRY-Cleanup, SW by-v1114
NEUE BACKEND-MODULE:

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

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

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

UI.JS ERWEITERUNGEN:

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

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

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

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

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

Tests 19/19 grün.

Hinweis: Migrationen für MEDIA_DIR (19 Stellen), API-Timeouts (12),
Date-Formatter im Frontend (24) und UI.text.truncate (5) sind als
Folge-Sprints möglich. Helper sind verfügbar.
2026-05-27 11:19:06 +02:00
e7939ce98e Bündel A-D: Race-Fixes, JWT-Cleanup, Storage-Watchdog, HTTPException, SW by-v1112
A — Founder-Number-Race (Audit-Fund aus Agent 2)
- partner.py PATCH /admin/users: SELECT COUNT + UPDATE+1 →
  atomares UPDATE mit Sub-Query. WHERE-Klausel prüft Limit + dass
  User noch nicht is_founder=1 ist. rowcount=0 → 'Plätze vergeben'.
- dogs.py POST /dogs (erster Hund triggert Gründer-Aktivierung): selbes
  Pattern. Zusätzlich AND is_founder_pending=1 als Schutz.
- Sub-Queries werden gegen Snapshot VOR dem UPDATE evaluiert
  (SQL-Spec), daher keine 'doppelte Nummer' möglich auch wenn zwei
  User gleichzeitig den ersten Hund anlegen.

B — JWT-Blacklist-Cleanup-Job
- _purge_expired_jwt() in auth.py existierte schon, war aber nicht
  verdrahtet → jwt_blacklist wuchs monoton.
- Neuer Scheduler-Job _job_purge_jwt_blacklist täglich 03:30
  (nach poison_archive, in ruhiger Zeit), mit _log_job für
  Error-Digest.

C — iOS Storage-Quota-Watchdog (PWA-Stabilität)
- offline-indicator.js: _checkStorageQuota() per
  navigator.storage.estimate() beim Init + alle 60s im Interval.
- Bei >=80% Auslastung: Tile-Cache auf 100 Einträge trimmen (statt
  default 500). Verhindert QuotaExceededError auf iOS-PWA (~50MB).
- Bei >=90%: einmaliger Toast-Hinweis pro Session
  'Speicher fast voll — Tiles werden gelöscht'.

D — HTTPException in osm.py
- 'raise Exception("Alle Overpass-Instanzen fehlgeschlagen")' wurde
  zu HTTP 500 → User-unfriendly. Jetzt 503 mit klarer Message
  'Kartendaten gerade nicht verfügbar'.

Tests 19/19 grün.
2026-05-27 09:41:56 +02:00
15d319fbd5 Admin: POI-Statistik erweitert (Nutzer-POIs nach Typ + Labels), SW by-v1099
- /admin/stats liefert jetzt zusätzlich user_poi_total + user_poi_by_type
  (User-POIs aus user_map_pois aufgeschlüsselt, komma-separierte Typen
  werden einzeln gezählt)
- Admin System-Tab zeigt zwei Karten:
  · OSM-Cache nach Typ (was Overpass-Cache enthält)
  · Nutzer-POIs nach Typ (selbst erstellte Marker)
- Interne Typ-Namen werden in lokalisierte Labels gemappt
  (tierarzt → Tierarzt, hundesalon → Hundesalon, etc.)
- Header der Karten zeigt Gesamtzahl inline
2026-05-26 21:37:35 +02:00
cc4f030fd0 Feature: Hundesalons in der Karte, SW by-v1098
- OSM-Query 'hundesalon' nutzt shop=pet_grooming + craft=pet_grooming
- map.js: neuer Layer 'hundesalon' mit Schere-Icon (#EC4899 Pink),
  in Layer-Liste, TYPEN, OSM_LAYER_MAP und PIN_TYPES eingetragen
- User können selbst Hundesalons als POI anlegen
  (places.py TYPEN + osm.py ALLOWED_TYPES erweitert)
2026-05-26 21:13:16 +02:00
9394bab1fb Big Sweep: Security + Race-Conditions + Tests + DSGVO + A11y, SW by-v1095
SECURITY (auth.py, routes/auth.py, database.py, main.py)
- JWT bekommt jti; Logout trägt in neue jwt_blacklist-Tabelle ein,
  decode_token() prüft → server-side Invalidierung
- JWT-Expiry default 30 → 7 Tage (ENV JWT_EXPIRY_DAYS überschreibt)
- Sliding-Refresh-Middleware: erneuert Cookie wenn >50% verbraucht
  (Schwelle via JWT_REFRESH_FRACTION, Default 2)
- Login-Lockout in DB-Tabelle login_attempts (5 Versuche / 15 Min,
  überlebt Container-Restart) — alte In-Memory-Lockouts ersetzt
- SMTP-Versand: alle 'except: pass' durch logger.exception ersetzt;
  Fehlversuche landen in failed_emails-Tabelle für späteres Retry
- Referral-Counter Race gefixt: UPDATE partner_codes SET uses=uses+1
  ... WHERE uses<max_uses RETURNING — atomar statt SELECT+UPDATE

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

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

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

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

NEUE ENV-VARS
- JWT_REFRESH_FRACTION (Default 2)
- JWT_EXPIRY_DAYS Default geändert (30 → 7)
2026-05-26 20:12:01 +02:00
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
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
738571d958 Fix: Verlauf-Tab kein Endlos-Spinner + Save-422 bei null-Feldern
- Verlauf: _verlaufLoading-Flag verhindert parallele Loads
- Verlauf: el nach await neu holen (stale DOM-Referenz nach Re-Render)
- Verlauf: bei _renderContent() Shell nur rendern wenn keine Sessions im Cache
- Backend: hund_stimmung/zufriedenheit als Optional[str/int] → akzeptiert null
2026-05-19 18:28:56 +02:00
cc841ef6d7 Feature: Trainingsprotokoll-Tab in Übungen, kein Tagebuch-Spam
- Neuer Tab 'Protokoll' in der Übungen-Seite: zeigt alle Trainingseinheiten
  chronologisch nach Datum gruppiert (Heute/Gestern/Datum-Label)
- Jede Einheit: Übungsname, Wdh., Erfolgs-Emoji, Stimmung, Sterne, Notiz, TOP-Badge
- 'Weitere laden' Pagination (30 Einheiten pro Seite)
- Backend: Training erstellt keine Tagebuch-Einträge mehr (weder bei ist_top noch manuell)
- Frontend: 'Als Meilenstein ins Tagebuch' Checkbox komplett entfernt
- onDogChange setzt Verlauf-State zurück
2026-05-19 18:17:50 +02:00
a1c4d2ab2b Fix: /me-Endpoint gibt geburtstag zurück (war vergessen) 2026-05-16 12:57:19 +02:00
b54d9fda99 Fix: users.geburtstag Migration + Format TT.MM statt MM-DD
- database.py: ALTER TABLE users ADD COLUMN geburtstag TEXT (fehlte!)
- profile.py: Validierung auf \d{2}\.\d{2} (TT.MM Format)
- settings.js: Placeholder/Pattern auf TT.MM geändert
- worlds.js: Birthday-Check auf DD.MM Format angepasst
SW by-v1029, APP_VER 1029
2026-05-16 12:02:52 +02:00
a4377033ec Feature: User-Geburtstag im Profil + Glückwunsch in JETZT-Welt
Settings:
- Feld 'Dein Geburtstag (optional)' im Profil-Formular (Format MM-TT)
- Hinweis: nur für Geburtstagsgrüße, kein Jahr nötig
- profile.py: geburtstag gespeichert + Format-Validierung MM-DD

JETZT-Welt wenn heute User-Geburtstag:
- Greet-Text: 'Herzlichen Glückwunsch' statt Tageszeit-Gruß
- Animiertes Geburtstags-Reminder-Card (confetti + cake Icons)
- 'Alles Gute zum Geburtstag, [Name]!'

SW by-v1028, APP_VER 1028
2026-05-16 11:55:34 +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
c59326af17 Fix+Polish: Phosphor-Icons Danke-Overlay, Quartalsbericht paid_amount
Giftköder Danke-Overlay (poison.js):
- Emoji 🚨/🐾/📡 durch Phosphor-Icons ersetzt: siren, paw-print, wifi-slash

Quartalsbericht (invoices.py + admin.js):
- Backend: _effective_gross() — für bezahlte Rechnungen wird paid_amount statt
  amount_gross für die Quartalssumme verwendet (Kulanz/Teilzahlung korrekt)
- Admin-Preview: effectiveAmt in der Vorschau-Tabelle, bei Abweichung Hinweis
  "(RG: xx,xx €)" für Nachvollziehbarkeit
- CSV: Spalte "Betrag (eingegangen)" + separate Spalte "Rechnungsbetrag"
- SW by-v995, APP_VER 995
2026-05-15 18:18:22 +02:00
3fae57a0e2 Feat: Kontaktformular im Impressum + /api/contact Endpoint ohne Auth (SW by-v986) 2026-05-15 16:46:37 +02:00
e714580d77 Feat: Cashflow auf paid_amount, Differenz-Badge, Kulanz-Abschreibung im Bezahlt-Modal (SW by-v984) 2026-05-15 16:06:08 +02:00
0f6b5afd6a Fix: Syntaxfehler in invoices.py — fehlende schließende Klammer bei _s() 2026-05-15 15:56:25 +02:00
68fd9c0e38 Fix: En-Dash in PDF durch Bindestrich ersetzen + _s() Sanitizer für alle Texteingaben (SW by-v982) 2026-05-15 15:50:02 +02:00
78f3077317 UX: Freischalten zeigt Rechnungsentwurf-Nummer im Toast + Confirm-Hinweis (SW by-v980) 2026-05-15 14:01:45 +02:00
49e01683ad Fix: 'Fällig bis' Umlaut korrigiert im PDF-Info-Block 2026-05-15 13:51:26 +02:00
ed6dd8da13 Fix: Quartalssumme korrekt (alle inkl. Storno), Netto ausgeblendet (SW by-v977) 2026-05-15 13:38:08 +02:00
6104132714 Feat: Quartalsbericht — Stornozeilen mit Minusbeträgen, nach Datum sortiert, Summen netten sich heraus (SW by-v976) 2026-05-15 13:27:05 +02:00
b10b3140eb Fix: Stornorechnung referenziert Originalnummer + Datum (§14 UStG Pflicht) 2026-05-15 13:18:32 +02:00
cabb2fd6f7 Fix: iOS Modal scrollIntoView bei Tastatur; CSV Stornierte mit 0€ + Stornonummer (SW by-v975) 2026-05-15 13:15:49 +02:00
8f3d639e34 Fix: Stornierte/Entwurfs-Rechnungen aus Quartalsbericht und Cashflow-Summen ausgeschlossen 2026-05-15 12:49:04 +02:00
aea5f04bc1 Fix: Von-Gründer-eingeladen → 100% dauerhaft (statt 50%); SW by-v972 2026-05-15 12:25:41 +02:00
2163169b73 Feat: Rabattsystem in Rechnungserstellung integriert (Gründer/Referral)
- _get_discount_info() Hilfsfunktion in admin.py (Gründer 100%, Referral-Stufen 20/30/50%, von Gründer eingeladen 50%)
- list_upgrade_requests liefert discount_pct + discount_reason pro User
- GET /admin/users/{user_id}/discount Endpoint
- _handle_upgrade_invoices nutzt Rabatt für amount_net/discount_pct/after_disc + passende Notiz
- scheduler.py _create_renewal_invoice_draft: inline Rabattberechnung + korrekte Beträge
- admin.js: Discount-Badge in Upgrade-Card, data-Attribute am Invoice-Button, _discountNote(), discount_pct + notes im Modal vorbelegt
2026-05-15 12:21:33 +02:00
699926cd76 Fix: Rechnung-Hinweistext auf AGB-konforme Jahresbeitrags-Notiz umgestellt
Alle drei Rechnungs-Einstiegspunkte (Admin-Upgrade-Button, automatische
Verlängerung via Scheduler, manuelles Upgrade via admin.py) erhalten jetzt
den einheitlichen Hinweis zum Jahresbeitrag gem. AGB ohne Rückerstattung.
2026-05-15 12:06:05 +02:00
a9f7923716 Feat: Kündigung blockt Erneuerungsentwurf; Upgrade storniert alte Rechnungen + legt neuen Entwurf an 2026-05-15 12:00:27 +02:00
b14a251bdc Feat: Entwurf bearbeiten (PATCH), erneut senden; SW by-v968 2026-05-15 11:33:48 +02:00
a2d089bce4 Fix: SMTP_SUPPORT_PASS Fallback in mailer, scaninput Logging 2026-05-15 11:29:16 +02:00
41a4808ad5 Fix: Storno sendet PDF+Mail+Scaninput; /scaninput Volume in staging ergänzt 2026-05-15 11:18:48 +02:00
8e36eb0611 Fix: PDF — breiter Header-Balken mit Logo, eine Seite, Hinweis-Prefix, Footer fix 2026-05-15 11:13:28 +02:00
0a466ef6ce Feat: Rechnungsadresse — Profil, Upgrade-Modal Hinweis, Rechnung-erstellen-Button in Upgrade-Cards (SW by-v967) 2026-05-15 10:59:12 +02:00
95b70d5119 Fix: Rechnungs-PDF komplett neu — DIN-5008-Layout, Überlagerung behoben, Bankverbindung, Footer, Deutsche Formatierung 2026-05-15 10:45:49 +02:00
5dddacff96 Feat: Admin action-items invoices_unpaid, CSV-Download, Quartalsbericht-Versand 2026-05-15 10:06:04 +02:00
77093774f9 Feat: Admin-Rechnungs-Endpoints — invoices_unpaid, CSV-Download, Quartalsbericht-Versand
- action_items(): invoices_unpaid (status='sent') zum Return-Dict hinzugefügt
- GET /api/admin/invoices/quarterly/{year}/{q}/csv — CSV-Download (Semikolon-getrennt, UTF-8-BOM für Excel)
- POST /api/admin/invoices/send-quarterly-report — sendet CSV-Anhang an Steuerberater + Zusammenfassung an René (SMTP_FROM); graceful fallback wenn attachments noch nicht von mailer.py unterstützt
2026-05-15 10:04:46 +02:00
b68a12587a Feature: Rechnungs-System (invoices) — Backend komplett
- DB-Migration: invoices + invoice_items Tabellen inkl. Indizes
- routes/invoices.py: vollständiger Admin-Router (prefix /api/admin/invoices)
  - CRUD: Liste, Detail, Erstellen, Senden, Bezahlen, Stornieren
  - PDF-Generierung via fpdf2 mit §14-UStG-Pflichtangaben (Kleinunternehmer-Hinweis)
  - Cashflow-Übersicht und Quartalsbericht
  - PDF-Download-Endpunkt
  - Speicherung in /scaninput + optionaler Paperless-Upload
- mailer.py: send_email() + Backends um optionale PDF-Anhänge erweitert
  (Brevo: base64, SMTP: MIMEApplication)
- main.py: invoices_router registriert
- docker-compose.yml: /volume1/scaninput:/scaninput Volume hinzugefügt
2026-05-15 10:04:23 +02:00
c032b9a3fb Fix: Stats-Kotbeutel → waste_basket Typ in osm_pois (zeigt echte Mülleimer-Zahl) 2026-05-14 22:47:14 +02:00
6495a5ff6b Fix: Stats-Kotbeutel aus osm_pois statt user_map_pois (zeigt 125.750 statt 2) 2026-05-14 22:45:50 +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