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
| ${h} | ` - ).join('')} -|||||||
|---|---|---|---|---|---|---|---|
| Noch keine Züchter | |||||||
| - Keine offenen Anfragen - |
- 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 = (() => {