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 9f5a2c4..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 = "921" # 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/routes/admin.py b/backend/routes/admin.py index 19e93eb..92a199d 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -124,17 +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] - upgrades_pending = conn.execute( - "SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL" - ).fetchone()[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, } @@ -1095,66 +1091,3 @@ async def generate_media_previews(user=Depends(require_admin)): errors += 1 return {"generated": generated, "skipped": skipped, "errors": errors} - - -# ------------------------------------------------------------------ -# 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"]} 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 3728277..1afe535 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -183,27 +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 AND is_zucht_hund=1) AS zuchthunde_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 bp.verified_at DESC NULLS LAST, 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/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 f75b2cf..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`, {}); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1aa2c36..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 = '921'; // ← 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 0f03bb4..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.zuchthunde_count || 0} Zuchthunde - - ${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/settings.js b/backend/static/js/pages/settings.js index 96ee9f7..8767cc6 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -77,137 +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 features = isPro - ? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features'] - : ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport']; - - const featureList = features.map(f => - `
  • ✓ ${f}
  • ` - ).join(''); - - 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. -
    -
    `, - footer: ` - - ` - }); - - document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('upgrade-request-send-btn'); - if (!btn) return; - 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.'); - } - } catch (e) { - btn.disabled = false; - btn.textContent = 'Anfrage senden'; - UI.toast.error(e.message || 'Fehler beim Senden.'); - } - }); - } - // ---------------------------------------------------------- // RENDER // ---------------------------------------------------------- @@ -407,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. +
    + ` : ''}