diff --git a/backend/database.py b/backend/database.py index 2c37d10..5c5516a 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2341,24 +2341,6 @@ def _migrate(conn_factory): except Exception: pass - # upgrade_requests: Abo-Upgrade-Anfragen von Nutzern - try: - conn.execute(""" - CREATE TABLE IF NOT EXISTS upgrade_requests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, - tier TEXT NOT NULL, - message TEXT, - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), - fulfilled_at TEXT, - fulfilled_by INTEGER REFERENCES users(id) - ) - """) - conn.execute("CREATE INDEX IF NOT EXISTS idx_upgrade_req_pending ON upgrade_requests(fulfilled_at, created_at DESC)") - logger.info("Migration: upgrade_requests bereit.") - except Exception as e: - logger.warning(f"Migration upgrade_requests: {e}") - # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index bed3d6f..097edd7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "939" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -465,11 +465,10 @@ async def sitemap(): today = date.today().isoformat() urls = [ ("https://banyaro.app/", "weekly", "1.0"), - ("https://banyaro.app/zuechter", "weekly", "0.9"), - ("https://banyaro.app/info", "monthly", "0.8"), - ("https://banyaro.app/presse", "monthly", "0.7"), + ("https://banyaro.app/info", "monthly", "0.9"), + ("https://banyaro.app/presse", "monthly", "0.8"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), - ("https://banyaro.app/knigge", "monthly", "0.7"), + ("https://banyaro.app/knigge", "monthly", "0.8"), ("https://banyaro.app/wurfboerse", "daily", "0.8"), ] diff --git a/backend/media_utils.py b/backend/media_utils.py index 4fa6a82..4cb2e28 100644 --- a/backend/media_utils.py +++ b/backend/media_utils.py @@ -56,12 +56,7 @@ def safe_media_path(media_dir: str, url: str) -> str | None: Konstruiert einen sicheren Dateipfad aus einer gespeicherten URL. Gibt None zurück wenn der Pfad außerhalb von media_dir liegt (Path-Traversal-Schutz). """ - if url.startswith("/media/"): - relative = url[len("/media/"):] - elif url.startswith("/"): - relative = url[1:] - else: - relative = url + relative = url.lstrip("/media/").lstrip("/") candidate = os.path.realpath(os.path.join(media_dir, relative)) real_base = os.path.realpath(media_dir) if not candidate.startswith(real_base + os.sep) and candidate != real_base: diff --git a/backend/routes/admin.py b/backend/routes/admin.py index b8cfb40..92a199d 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -124,20 +124,13 @@ async def action_items(user=Depends(require_mod)): users_today = conn.execute( "SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')" ).fetchone()[0] - try: - upgrades_pending = conn.execute( - "SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL" - ).fetchone()[0] - except Exception: - upgrades_pending = 0 return { - "jobs_pending": jobs, - "breeder_pending": breeders, - "reports_open": reports, - "fotos_pending": fotos, - "poi_edits_pending": poi_edits, - "users_today": users_today, - "upgrades_pending": upgrades_pending, + "jobs_pending": jobs, + "breeder_pending": breeders, + "reports_open": reports, + "fotos_pending": fotos, + "poi_edits_pending": poi_edits, + "users_today": users_today, } @@ -1061,25 +1054,21 @@ async def ors_stats(user=Depends(require_mod)): @router.post("/media/generate-previews") async def generate_media_previews(user=Depends(require_admin)): - """Generiert fehlende _preview.webp für alle Bilder in /data/media.""" - import logging as _log + """Generiert fehlende _preview.jpg für alle Bilder in /data/media.""" + import io as _io from media_utils import generate_preview, _PREVIEW_EXTS - _logger = _log.getLogger(__name__) MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") generated = 0 skipped = 0 errors = 0 - dirs_info = {} - for subdir in ("diary", "forum", "breeds", "breeds/gallery", "breeds/submissions"): + for subdir in ("diary", "forum"): folder = os.path.join(MEDIA_DIR, subdir) if not os.path.isdir(folder): - dirs_info[subdir] = "not found" continue - files = os.listdir(folder) - dirs_info[subdir] = f"{len(files)} files" - for fname in files: + for fname in os.listdir(folder): + # Nur Original-Bilder (keine _preview, _thumb, Videos, PDFs) low = fname.lower() if "_preview" in low or "_thumb" in low: continue @@ -1098,73 +1087,7 @@ async def generate_media_previews(user=Depends(require_admin)): generated += 1 else: skipped += 1 - _logger.warning(f"Preview None für {subdir}/{fname}") except Exception as exc: errors += 1 - _logger.error(f"Preview-Fehler {subdir}/{fname}: {exc}") - _logger.info(f"generate-previews: {generated} neu, {skipped} vorhanden, {errors} Fehler | dirs: {dirs_info}") - return {"generated": generated, "skipped": skipped, "errors": errors, "dirs": dirs_info} - - -# ------------------------------------------------------------------ -# GET /api/admin/upgrade-requests — offene Upgrade-Anfragen -# POST /api/admin/upgrade-requests/{id}/fulfill — Tier setzen + Mail -# ------------------------------------------------------------------ -@router.get("/upgrade-requests") -async def list_upgrade_requests(user=Depends(require_admin)): - with db() as conn: - rows = conn.execute(""" - SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at, - u.name, u.email - FROM upgrade_requests r - JOIN users u ON u.id = r.user_id - ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC - LIMIT 100 - """).fetchall() - return [dict(r) for r in rows] - - -@router.post("/upgrade-requests/{req_id}/fulfill") -async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): - with db() as conn: - req = conn.execute( - "SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?", - (req_id,) - ).fetchone() - if not req: - raise HTTPException(404, "Anfrage nicht gefunden.") - if req["fulfilled_at"]: - raise HTTPException(400, "Bereits erledigt.") - if req["tier"] not in _VALID_TIERS: - raise HTTPException(400, "Ungültiger Tier.") - conn.execute( - "UPDATE users SET subscription_tier=? WHERE id=?", - (req["tier"], req["user_id"]) - ) - conn.execute( - "UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?", - (user["id"], req_id) - ) - _audit(conn, user, "fulfill_upgrade", f"user:{req['user_id']}", f"tier={req['tier']}") - - tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"} - tier_label = tier_labels.get(req["tier"], req["tier"]) - try: - from mailer import send_email, email_html - body_html = f""" -

Hallo {req['name']},

-

dein Account wurde soeben auf {tier_label} freigeschaltet.

-

Du kannst alle {tier_label}-Features ab sofort in der App nutzen. - Öffne Ban Yaro und lade die App einmal neu — dann ist dein neuer Tarif aktiv.

-

Vielen Dank für dein Vertrauen!

-

Viele Grüße
René & das Ban Yaro Team

""" - html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen") - plain = (f"Hallo {req['name']},\n\ndein Account wurde auf {tier_label} freigeschaltet.\n" - f"Öffne Ban Yaro und lade die App neu.\n\nViele Grüße\nRené") - await send_email(req["email"], f"Dein {tier_label}-Zugang ist aktiv", html, plain) - except Exception as e: - import logging - logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}") - - return {"ok": True, "tier": req["tier"], "user": req["name"]} + return {"generated": generated, "skipped": skipped, "errors": errors} diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 863be66..a0174e6 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -240,7 +240,7 @@ async def me(user=Depends(get_current_user)): profil_sichtbarkeit, avatar_url, created_at, is_founder, is_partner, founder_number, is_founder_pending, notes_ki_enabled, gassi_stunde_push, - preferred_theme, subscription_tier + preferred_theme FROM users WHERE id=?""", (user["id"],) ).fetchone() @@ -335,46 +335,6 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request): return {"ok": True} -class UpgradeRequestBody(BaseModel): - tier: str - message: Optional[str] = None - -@router.post("/upgrade-request") -async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)): - _VALID = {"pro", "breeder"} - if data.tier not in _VALID: - raise HTTPException(400, "Ungültiger Tarif.") - with db() as conn: - existing = conn.execute( - "SELECT id FROM upgrade_requests WHERE user_id=? AND tier=? AND fulfilled_at IS NULL", - (user["id"], data.tier) - ).fetchone() - if existing: - return {"ok": True, "already": True} - conn.execute( - "INSERT INTO upgrade_requests (user_id, tier, message) VALUES (?,?,?)", - (user["id"], data.tier, data.message or None) - ) - email = conn.execute("SELECT email FROM users WHERE id=?", (user["id"],)).fetchone()["email"] - - tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"} - tier_label = tier_labels[data.tier] - admin_email = os.getenv("ADMIN_EMAIL", "") - if admin_email: - try: - from routes.outreach import _send_smtp - subject = f"[Ban Yaro] Upgrade-Anfrage: {tier_label} — {user['name']}" - body = (f"Neue Upgrade-Anfrage:\n\n" - f"Nutzer: {user['name']} ({email})\n" - f"Tarif: {tier_label}\n" - f"Nachricht: {data.message or '—'}\n\n" - f"Admin-Panel: https://banyaro.app/#admin") - _send_smtp(admin_email, subject, body, "support") - except Exception: - pass - return {"ok": True} - - @router.post("/reset-password") async def reset_password(data: ResetPasswordRequest, request: Request): rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw") diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index fe4028b..1afe535 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -87,7 +87,7 @@ async def breeder_apply( stadt: str = Form(...), website: str = Form(""), beschreibung: str = Form(""), - dokument: UploadFile = File(None), + dokument: UploadFile = File(...), user=Depends(get_current_user), ): with db() as conn: @@ -103,27 +103,28 @@ async def breeder_apply( if row["breeder_status"] == "pending": raise HTTPException(400, "Du hast bereits einen offenen Antrag.") - # Dokument optional speichern - filepath = None - if dokument and dokument.filename: - data = await dokument.read() - if len(data) > 10 * 1024 * 1024: - raise HTTPException(400, "Dokument zu groß (max. 10 MB).") - ext = os.path.splitext(dokument.filename)[1].lower() - if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"): - raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.") - user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"])) - os.makedirs(user_doc_dir, exist_ok=True) - filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}" - filepath = os.path.join(user_doc_dir, filename) - with open(filepath, "wb") as f: - f.write(data) + # Dokument validieren und speichern + data = await dokument.read() + if len(data) > 10 * 1024 * 1024: + raise HTTPException(400, "Dokument zu groß (max. 10 MB).") + ext = os.path.splitext(dokument.filename or "")[1].lower() + if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"): + raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.") + + user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"])) + os.makedirs(user_doc_dir, exist_ok=True) + + filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}" + filepath = os.path.join(user_doc_dir, filename) + with open(filepath, "wb") as f: + f.write(data) with db() as conn: conn.execute( "UPDATE users SET breeder_status='pending' WHERE id=?", (user["id"],) ) + # Profil-Entwurf anlegen (oder überschreiben wenn rejected) conn.execute( "INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) " "VALUES (?,?,?,?,?,?,?,?) " @@ -134,11 +135,10 @@ async def breeder_apply( "verified_at=NULL", (user["id"], zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) ) - if filepath: - conn.execute( - "INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)", - (user["id"], "antrag", filepath) - ) + conn.execute( + "INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)", + (user["id"], "antrag", filepath) + ) # Admin benachrichtigen admin_body = f""" @@ -183,28 +183,6 @@ async def admin_pending_breeders(admin=Depends(require_admin)): return [dict(r) for r in rows] -# ------------------------------------------------------------------ -# GET /api/admin/breeders — alle aktiven Züchter -# ------------------------------------------------------------------ -@router.get("/admin/breeders") -async def admin_all_breeders(admin=Depends(require_admin)): - with db() as conn: - rows = conn.execute(""" - SELECT u.id, u.name, u.email, u.created_at, u.subscription_tier, - u.breeder_status, u.last_login, - bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied, - bp.stadt, bp.website, bp.verified_at, - (SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count, - (SELECT COUNT(*) FROM dogs WHERE user_id=u.id) AS hunde_count - FROM users u - LEFT JOIN breeder_profiles bp ON bp.user_id = u.id - WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved' - ORDER BY CASE WHEN bp.verified_at IS NULL THEN 1 ELSE 0 END, - bp.verified_at DESC, u.created_at DESC - """).fetchall() - return [dict(r) for r in rows] - - # ------------------------------------------------------------------ # GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags # ------------------------------------------------------------------ diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 832e777..cc86caf 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -191,7 +191,6 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): raise HTTPException(404, "Hund nicht gefunden.") # Hintergrundfoto: Querformat-Bilder bevorzugt, tagesweise rotierend - # Ownership bereits durch Dog-Check oben gesichert (dog gehört user) photos = conn.execute( """SELECT dm.url FROM diary_media dm JOIN diary d ON d.id = dm.diary_id @@ -844,14 +843,13 @@ async def upload_photo( file: UploadFile = File(...), user=Depends(get_current_user) ): - # Hund gehört dem User? Altes Foto merken für späteres Löschen. + # Hund gehört dem User? with db() as conn: dog = conn.execute( - "SELECT id, foto_url FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) + "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) ).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") - old_foto_url = dog["foto_url"] # Datei immer als JPEG speichern (HEIC/PNG/WebP → kompatibel für alle Browser) import io @@ -885,15 +883,6 @@ async def upload_photo( with db() as conn: conn.execute("UPDATE dogs SET foto_url=? WHERE id=?", (foto_url, dog_id)) - # Altes Foto von Disk löschen - if old_foto_url: - try: - old_path = safe_media_path(MEDIA_DIR, old_foto_url) - if old_path and os.path.isfile(old_path): - os.remove(old_path) - except Exception: - pass - return {"foto_url": foto_url} diff --git a/backend/static/index.html b/backend/static/index.html index d079f75..0e13659 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -599,10 +599,10 @@ - - - - + + + + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 8621935..9781362 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -124,9 +124,6 @@ const API = (() => { return get('/auth/me'); }, referral: () => get('/auth/referral'), - upgradeRequest(tier, message) { - return post('/auth/upgrade-request', { tier, message }); - }, }; // ---------------------------------------------------------- @@ -691,7 +688,6 @@ const API = (() => { updateProfile(data) { return put('/breeder/profile', data); }, adminCreateProfile() { return post('/admin/breeder/create-profile', {}); }, pendingList() { return get('/admin/breeders/pending'); }, - allList() { return get('/admin/breeders'); }, documents(userId) { return get(`/admin/breeder/${userId}/documents`); }, documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; }, approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); }, @@ -800,17 +796,9 @@ const API = (() => { get(`/osm/pois?type=${type}&south=${south}&west=${west}&north=${north}&east=${east}&fast=true`), }; - // SW-Cache-Einträge für eine URL löschen (z.B. nach Foto-Upload) - async function swCacheDelete(path) { - try { - const c = await caches.open('ban-yaro-api-v1'); - await c.delete(new Request(path)); - } catch {} - } - // Öffentliche API return { - get, post, put, patch, del, upload, swCacheDelete, + get, post, put, patch, del, upload, auth, dogs, diary, health, tieraerzte, healthDocs, poison, places, routes, walks, events, sitting, forum, lost, knigge, weather, push, friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index cd79577..c6d2189 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '939'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '918'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 00a19e8..2747256 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -26,7 +26,6 @@ window.Page_admin = (() => { { id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' }, { id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' }, { id: 'referrals', label: 'Referrals', icon: 'share-network' }, - { id: 'upgrades', label: 'Upgrades', icon: 'crown-simple' }, ]; // ------------------------------------------------------------------ @@ -91,7 +90,6 @@ window.Page_admin = (() => { try { d = await API.get('/admin/action-items'); } catch { return; } const items = [ - { key: 'upgrades_pending', label: 'Upgrade-Anfragen', tab: 'upgrades', icon: 'crown-simple' }, { key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' }, { key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' }, { key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' }, @@ -165,7 +163,6 @@ window.Page_admin = (() => { case 'hilfe': await _renderHilfe(el); break; case 'uebungen_admin': await _renderUebungenAdmin(el); break; case 'referrals': await _renderReferrals(el); break; - case 'upgrades': await _renderUpgrades(el); break; } } catch (e) { el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.'); @@ -1893,17 +1890,12 @@ window.Page_admin = (() => { ${UI.icon('arrows-clockwise')} Aktualisieren -
Lade…
-
Lade…
+
Lade…
`; - el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => { - _loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')); - _loadZuechterListe(el.querySelector('#adm-zuchter-liste')); - }); - await Promise.all([ - _loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')), - _loadZuechterListe(el.querySelector('#adm-zuchter-liste')), - ]); + el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => + _loadZuechterAntraege(el.querySelector('#adm-zuchter-list')) + ); + await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list')); } async function _loadZuechterAntraege(el) { @@ -1917,20 +1909,12 @@ window.Page_admin = (() => { } if (!antraege.length) { - el.innerHTML = `
-
Offene Anträge
-
- ${UI.icon('check-circle')} Keine offenen Anträge -
-
`; + el.innerHTML = _emptyState('certificate', 'Keine offenen Anträge', 'Aktuell liegen keine Züchter-Anträge zur Prüfung vor.'); return; } el.innerHTML = ` -
-
Offene Anträge (${antraege.length})
-
-
+
${antraege.map(a => `
@@ -2085,74 +2069,6 @@ window.Page_admin = (() => { }); } - async function _loadZuechterListe(el) { - el.innerHTML = `
Lade…
`; - let breeders; - try { - breeders = await API.breeder.allList(); - } catch (e) { - el.innerHTML = _emptyState('warning', 'Fehler', e.message); - return; - } - - const tierBadge = t => { - if (t === 'breeder') return `Züchter-Abo`; - if (t === 'breeder_test') return `Test`; - return `Standard`; - }; - - const rows = breeders.map(b => ` - - -
${_esc(b.name)}
-
${_esc(b.email)}
- - ${_esc(b.zwingername || '—')} - ${_esc(b.rasse_text || '—')} - ${_esc(b.stadt || '—')} - - ${b.wuerfe_count || 0} Würfe
- ${b.hunde_count || 0} Hunde - - ${tierBadge(b.subscription_tier)} - - ${b.verified_at ? new Date(b.verified_at).toLocaleDateString('de-DE') : '—'} - - - - - `).join(''); - - el.innerHTML = ` -
-
Alle Züchter (${breeders.length})
-
- - - ${['Nutzer','Zwingername','Rasse','Stadt','Aktivität','Abo','Seit',''].map(h => - `` - ).join('')} - - - ${rows || ``} - -
${h}
Noch keine Züchter
-
-
`; - - el.querySelectorAll('.adm-breeder-tier-btn').forEach(btn => { - btn.addEventListener('click', () => - _changeTier(btn.dataset.uid, btn.dataset.name, btn.dataset.tier) - ); - }); - } - // ------------------------------------------------------------------ async function _renderJobs(el) { el.innerHTML = ` @@ -3503,104 +3419,6 @@ window.Page_admin = (() => {
`; } - // ------------------------------------------------------------------ - // TAB: UPGRADES - // ------------------------------------------------------------------ - async function _renderUpgrades(el) { - const rows = await API.get('/admin/upgrade-requests'); - - const tierBadge = t => { - const cfg = { pro: ['Pro', '#16a34a'], breeder: ['Züchter', '#C4843A'] }; - const [label, color] = cfg[t] || [t, '#888']; - return `${label}`; - }; - - const pending = rows.filter(r => !r.fulfilled_at); - const done = rows.filter(r => r.fulfilled_at); - - const _row = (r, showBtn) => ` - - ${_esc(r.name)}
- ${_esc(r.email)} - ${tierBadge(r.tier)} - - ${r.message ? _esc(r.message) : '—'} - - ${r.created_at?.slice(0,10) || ''} - - ${showBtn - ? `` - : `✓ ${r.fulfilled_at?.slice(0,10)}`} - - `; - - const thead = ` - ${['Nutzer','Tarif','Nachricht','Datum','Aktion'].map(h => - `${h}` - ).join('')}`; - - el.innerHTML = ` -
-
Offene Anfragen (${pending.length})
-
- - ${thead} - - ${pending.length - ? pending.map(r => _row(r, true)).join('') - : ``} - -
- Keine offenen Anfragen -
-
-
- ${done.length ? ` -
-
Erledigt (${done.length})
-
- - ${thead} - ${done.map(r => _row(r, false)).join('')} -
-
-
` : ''}`; - - el.querySelectorAll('.adm-fulfill-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const { id, name, tier } = btn.dataset; - const tierLabel = { pro: 'Pro', breeder: 'Züchter' }[tier] || tier; - const ok = await UI.modal.confirm({ - title: `${name} auf ${tierLabel} freischalten?`, - body: `

- Der Account wird auf ${tierLabel} gesetzt und - eine Bestätigungsmail gesendet. -

`, - confirmLabel: 'Freischalten', - danger: false, - }); - if (!ok) return; - btn.disabled = true; - btn.textContent = '…'; - try { - const res = await API.post(`/admin/upgrade-requests/${id}/fulfill`); - UI.toast.success(`${res.user} wurde auf ${tierLabel} freigeschaltet.`); - _renderTab(); - } catch (e) { - UI.toast.error(e.message); - btn.disabled = false; - btn.textContent = 'Freischalten'; - } - }); - }); - } - return { init, refresh, onDogChange }; })(); diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js index 95aa123..9399085 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -769,16 +769,8 @@ window.Page_dog_profile = (() => { await API.dogs.updatePhotoPosition(dog.id, 1.0, 0.0, 0.0); _appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 }; _appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d); - // localStorage + SW-Cache invalidieren - const userId2 = _appState.user?.id || 'anon'; - localStorage.removeItem(`w3_bg3_${userId2}_` + new Date().toISOString().slice(0, 10)); - localStorage.removeItem('w3_dogs'); - API.swCacheDelete('/api/dogs'); - API.swCacheDelete(`/api/dogs/${dog.id}`); - API.swCacheDelete(`/api/dogs/${dog.id}/welcome-dashboard`); UI.modal.close(); - App.renderDogSwitcher?.(); - window.Worlds?.refresh(_appState); + App.renderDogSwitcher(); UI.toast.success('Foto hochgeladen.'); _renderProfile(_appState.activeDog); // Editor neu öffnen damit User positionieren kann @@ -1362,9 +1354,6 @@ window.Page_dog_profile = (() => { fell_typ: fd.fell_typ || null, }; - // Datei-Referenz VOR Modal-Close sichern — DOM-Element wird beim Schließen entfernt - const fotoFile = document.getElementById('dp-form-foto')?.files[0]; - let saved; if (dog) { saved = await API.dogs.update(dog.id, payload); @@ -1382,6 +1371,7 @@ window.Page_dog_profile = (() => { } // Foto hochladen wenn gewählt + const fotoFile = document.getElementById('dp-form-foto')?.files[0]; if (fotoFile) { try { const fd = new FormData(); @@ -1390,13 +1380,6 @@ window.Page_dog_profile = (() => { saved.foto_url = result.foto_url; _appState.activeDog = { ...saved }; _appState.dogs = _appState.dogs.map(d => d.id === saved.id ? _appState.activeDog : d); - // localStorage + SW-Cache invalidieren damit Welten das neue Foto zeigen - const userId = _appState.user?.id || 'anon'; - localStorage.removeItem(`w3_bg3_${userId}_` + new Date().toISOString().slice(0, 10)); - localStorage.removeItem('w3_dogs'); - API.swCacheDelete('/api/dogs'); - API.swCacheDelete(`/api/dogs/${saved.id}`); - API.swCacheDelete(`/api/dogs/${saved.id}/welcome-dashboard`); } catch { UI.toast.warning('Profil gespeichert, Foto konnte nicht hochgeladen werden.'); } @@ -1405,9 +1388,6 @@ window.Page_dog_profile = (() => { // Dog Switcher in Header + Sidebar aktualisieren App.renderDogSwitcher?.(); - // Welten neu laden damit HUND-Welt den neuen Hund zeigt - window.Worlds?.refresh(_appState); - await _render(); }); }); diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index bc65d98..4cce79c 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -205,10 +205,8 @@ window.Page_map = (() => {
- ${App.hasPro(_appState?.user) ? ` - ` : ''}
@@ -291,8 +289,8 @@ window.Page_map = (() => { }); document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode); - document.getElementById('map-radar-btn')?.addEventListener('click', _toggleRadar); - document.getElementById('map-temp-btn')?.addEventListener('click', _toggleTemp); + document.getElementById('map-radar-btn').addEventListener('click', _toggleRadar); + document.getElementById('map-temp-btn').addEventListener('click', _toggleTemp); } // ---------------------------------------------------------- diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 1bacd62..8767cc6 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -77,269 +77,6 @@ window.Page_settings = (() => { } } - // ---------------------------------------------------------- - // ABO & TARIF - // ---------------------------------------------------------- - function _tierCard(u) { - const tier = u.subscription_tier || 'standard'; - const rolle = u.rolle || 'user'; - const isAdmin = rolle === 'admin' || rolle === 'moderator'; - const isPro = ['pro','pro_test'].includes(tier); - const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder'; - const isStandard = !isAdmin && !isPro && !isBreeder; - - const _badge = (label, color) => - `${label}`; - - const _upgradeBtn = (id, label, price, color) => - ``; - - let statusHtml = ''; - let actionsHtml = ''; - - if (isAdmin) { - statusHtml = _badge('Admin', '#6366f1'); - } else if (isBreeder) { - statusHtml = _badge('Züchter aktiv', '#C4843A'); - } else if (isPro) { - statusHtml = _badge('Pro aktiv', '#16a34a'); - actionsHtml = ` -
- ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')} -
`; - } else { - statusHtml = _badge('Kostenlos', '#888'); - actionsHtml = ` -
- ${_upgradeBtn('settings-upgrade-pro-btn','Ban Yaro Pro','29 €/Jahr','#16a34a')} - ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter','49 €/Jahr','#C4843A')} -
`; - } - - return ` -
-
Abo & Tarif
-
-
- Aktueller Tarif: - ${statusHtml} -
- ${actionsHtml} -
-
`; - } - - function _showUpgradeModal(tier) { - const isPro = tier === 'pro'; - const label = isPro ? 'Ban Yaro Pro' : 'Züchter'; - const price = isPro ? '29 €/Jahr' : '49 €/Jahr'; - const color = isPro ? '#16a34a' : '#C4843A'; - const _group = (title, items) => ` -
-
${title}
- ${items.map(f => ` -
- - ${f} -
`).join('')} -
`; - - const featureList = isPro - ? _group('Deine Hunde', [ - 'Bis zu 10 Hunde gleichzeitig verwalten', - 'Getrennte Trainingsfortschritte, Gesundheits- und Ernährungsdaten je Hund', - ]) - + _group('Community & Alltag', [ - 'Gassi-Treffen: Fotos und Rasse der Teilnehmer sichtbar, Fotos nach dem Treffen hochladen', - 'Direktnachrichten & Chat mit anderen Hundebesitzern', - 'Playdate: Spielkameraden in der Nähe finden und verabreden', - ]) - + _group('Tools & Wissen', [ - 'Ernährung: Kalorienbedarf-Rechner, BARF-Guide, Giftliste, KI-Ernährungsberater', - 'Reise-Checkliste & EU-Länder-Einreiseregeln', - 'Notizblock mit KI-Muster-Analyse', - 'Erweiterte Karten-Layer (Wandern, Radfahren, Satellit)', - 'Alle künftigen Pro-Features inklusive', - ]) - : `
- ✓ Alle Pro-Features inklusive — - mehrere Hunde, Ernährung, Gassi-Community, Chat, Playdate, Reise, Karten-Layer -
` - + _group('Zucht-Management', [ - 'Zuchtkartei: Stammdaten, Gesundheitstests (HD, ED, OCD, Augen, Herz, Patella, ZTP), Gentests (MDR1, PRA, DM, vWD)', - 'Wurfverwaltung: Welpen, Gewichtsverlauf, Fotos, automatisch ausgefüllter Kaufvertrag', - 'Warteliste: Interessenten mit Präferenzen (Geschlecht, Farbe, Verwendungszweck) pro Zuchthündin', - 'Läufigkeit & Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, Meilensteinberechnung', - 'Wurf-Buchstabe und Wurf-Name (z. B. A-Wurf, „Vatertags-Wurf")', - ]) - + _group('KI & Analyse', [ - 'KI-Züchter-Assistent: Wurfankündigungen schreiben, Genetik-Erklärung für Käufer, Paarungsanalyse', - 'Stammbaum bis 4 Generationen mit klickbaren Knoten', - 'Inzucht-Koeffizient (Wright\'s Formel, Ampel-Bewertung, Probeverpaarung)', - 'Tierschutz-Check automatisch bei jeder Verpaarung', - 'KI-Jahresbericht mit Trends und Empfehlungen', - ]) - + _group('Sichtbarkeit & Export', [ - 'Öffentliches Züchter-Profil unter banyaro.app/breeder/{zwingername}', - 'Wurfbörse: Würfe öffentlich ankündigen, Käufer schreiben direkt an', - 'Datenexport als HTML-Dossier und ODS-Tabelle (LibreOffice / Excel)', - 'Privater Züchter-Bereich mit Zwingername und Logo', - ]); - - const inputStyle = `width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3); - border:1.5px solid var(--c-border);border-radius:var(--radius-md); - font-size:var(--text-sm);font-family:inherit;background:var(--c-surface);color:var(--c-text)`; - - const breederForm = isPro ? '' : ` -
-
- Dein Zwinger -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
- Zuchtbuch, Vereinsausweis o.ä. — kann auch per E-Mail nachgereicht werden -
-
-
-
`; - - UI.modal.open({ - title: `${label} freischalten`, - body: ` -
-
-
${price}
-
- Einmaliger Jahresbeitrag
Kündigung jederzeit möglich -
-
-
- ${featureList} -
-
- Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden. - Wir melden uns mit den Zahlungsdetails per E-Mail. -
- ${breederForm} -
`, - footer: ` - - ` - }); - - document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('upgrade-request-send-btn'); - if (!btn) return; - - // Züchter: Formular validieren + als FormData senden - if (!isPro) { - const form = document.getElementById('breeder-upgrade-form'); - if (form && !form.reportValidity()) return; - if (form) { - const fd = new FormData(form); - fd.set('vdh_mitglied', form.querySelector('[name="vdh_mitglied"]').checked ? '1' : '0'); - // Pflichtfelder aus Form übernehmen falls leer → leere Strings senden - if (!fd.get('verein')) fd.set('verein', ''); - if (!fd.get('stadt')) fd.set('stadt', ''); - btn.disabled = true; - btn.textContent = 'Wird gesendet…'; - try { - await API.breeder.apply(fd); - } catch (e) { - if (!e.message?.includes('bereits')) { - btn.disabled = false; - btn.textContent = 'Anfrage senden'; - UI.toast.error(e.message || 'Fehler beim Einreichen.'); - return; - } - } - } - } else { - btn.disabled = true; - btn.textContent = 'Wird gesendet…'; - } - - try { - const res = await API.auth.upgradeRequest(tier); - UI.modal.close(); - if (res.already) { - UI.toast.info('Deine Anfrage liegt bereits vor — wir melden uns bald.'); - } else { - UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.'); - } - if (!isPro) _loadBreederCard(); - } catch (e) { - btn.disabled = false; - btn.textContent = 'Anfrage senden'; - UI.toast.error(e.message || 'Fehler beim Senden.'); - } - }); - } - // ---------------------------------------------------------- // RENDER // ---------------------------------------------------------- @@ -539,6 +276,13 @@ window.Page_settings = (() => { Feedback geben
+ ${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? ` +
+ ⭐ Ban Yaro Pro kommt bald — mehr Features, mehrere Hunde. +
+ ` : ''}
`; } else { - // Kein Antrag, kein Profil — Card ausblenden (Upgrade-Flow läuft über Abo & Tarif) - slot.innerHTML = ''; - return; + actionBlock = ` +
+ +
`; } slot.innerHTML = ` @@ -1464,7 +1190,11 @@ window.Page_settings = (() => {
`; // Button-Handler binden - slot.querySelector('#breeder-reapply-btn')?.addEventListener('click', () => _showUpgradeModal('breeder')); + const applyBtn = slot.querySelector('#breeder-apply-btn'); + const reapplyBtn = slot.querySelector('#breeder-reapply-btn'); + if (applyBtn || reapplyBtn) { + (applyBtn || reapplyBtn).addEventListener('click', () => _openBreederApplyModal()); + } slot.querySelector('#breeder-edit-profile-btn')?.addEventListener('click', () => _openBreederEditModal(profile) diff --git a/backend/static/js/pages/wiki.js b/backend/static/js/pages/wiki.js index 86bb8d9..b0c101d 100644 --- a/backend/static/js/pages/wiki.js +++ b/backend/static/js/pages/wiki.js @@ -384,13 +384,8 @@ window.Page_wiki = (() => { function _breedCardHtml(r) { const fotoUrl = r.foto_url || r.user_foto || ''; - // Für lokale Bilder: _preview.webp zuerst, bei Fehler Original nachladen - const srcUrl = fotoUrl.startsWith('/media/') - ? fotoUrl.replace(/\.(jpe?g|png|gif|webp)$/i, '_preview.webp') - : fotoUrl; const photoHtml = fotoUrl - ? `${_esc(r.name)}` + ? `${_esc(r.name)}` : ''; const fallbackHtml = `
${_DOG_SILHOUETTE}
`; @@ -751,9 +746,7 @@ window.Page_wiki = (() => { ${allFotos.map((f, i) => ` `).join('')}
` : ''} diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index b250a29..0c3c15d 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -5,12 +5,11 @@ window.Worlds = (() => { - let _state = null; - let _cur = 1; // 0=JETZT 1=HUND 2=WELT - let _visible = false; - let _map = null; - let _weltInited = false; - let _refreshPending = false; // gesetzt wenn refresh() während !_visible aufgerufen wird + let _state = null; + let _cur = 1; // 0=JETZT 1=HUND 2=WELT + let _visible = false; + let _map = null; + let _weltInited = false; let _lastUserId = undefined; let _dogs = []; // gecachte Hundesliste let _dogIdx = 0; // aktuell angezeigter Hund @@ -119,14 +118,6 @@ window.Worlds = (() => { if (worldIdx != null) _goTo(worldIdx, false); if (_cur === 2 && !_weltInited) { _weltInited = true; _renderWelt(); } - // Ausstehender Refresh (z.B. nach Foto-Upload während Worlds unsichtbar) - if (_refreshPending) { - _refreshPending = false; - _renderJetzt(); - _renderHund(); - return; - } - // Nach Login/Logout: Config aus DB laden, dann rendern const currentUserId = _state?.user?.id ?? null; if (currentUserId !== _lastUserId) { @@ -356,6 +347,7 @@ window.Worlds = (() => {
${chips.map(c => ` - in deinem Profil. - -
`; @@ -412,10 +388,6 @@ window.Worlds = (() => { navigateTo(btn.dataset.page); }); }); - ov.querySelector('#fab-all-goto-worlds')?.addEventListener('click', () => { - _close(); - _openConfigModal(); - }); } // ── SCHNELL-GASSI ───────────────────────────────────────────── @@ -701,7 +673,6 @@ window.Worlds = (() => { let cfg = JSON.parse(JSON.stringify(_getConfig())); // deep copy let _drag = null; // { page, fromWorld, ghost } - const isAdmin = _state?.user?.rolle === 'admin'; const worldColors = { jetzt:'rgba(196,132,58,0.6)', hund:'rgba(196,132,58,0.8)', welt:'rgba(99,130,220,0.6)' }; const worldLabels = { jetzt:'JETZT', hund:'HUND', welt:'WELT', pool:'Nicht verwendet' }; const allAssigned = () => new Set([...cfg.jetzt, ...cfg.hund, ...cfg.welt]); @@ -805,8 +776,7 @@ window.Worlds = (() => { `} - ${isAdmin && c.pro ? `P` : ''} - ${isAdmin && c.role === 'breeder' ? `Z` : ''} + ${c.pro && _isRoleBasedPro() ? `P` : ''} @@ -953,8 +923,7 @@ window.Worlds = (() => { async function _loadDailyImage(dog) { if (!dog) return null; - const userId = _state?.user?.id || 'anon'; - const todayKey = `bg3_${userId}_` + new Date().toISOString().slice(0, 10); + const todayKey = 'bg3_' + new Date().toISOString().slice(0, 10); const cached = _wLoad(todayKey); if (cached?.data) return cached.data; try { @@ -1188,7 +1157,7 @@ window.Worlds = (() => {
Deine Bereiche
- ${features.map(f => _chip(f.icon, f.label, f.page, false, false, false)).join('')} + ${features.map(f => _chip(f.icon, f.label, f.page, false, f.pro && _isRoleBasedPro(), f.role === 'breeder')).join('')}