diff --git a/Dockerfile b/Dockerfile index c29da6b..623ddef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ RUN mkdir -p /data/media/dogs /data/media/diary /data/media/poison \ EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"] +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] diff --git a/backend/auth.py b/backend/auth.py index d923c70..b3365aa 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -87,7 +87,7 @@ def get_current_user( user_id = int(payload["sub"]) with db() as conn: row = conn.execute( - "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner FROM users WHERE id=?", + "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status FROM users WHERE id=?", (user_id,) ).fetchone() diff --git a/backend/database.py b/backend/database.py index e428a56..940c76f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -559,10 +559,6 @@ def _migrate(conn_factory): ("users", "ki_zucht_paarung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"), - # Partner-Code + Gründer-Lizenz - ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"), - ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), - ("users", "founder_number", "INTEGER"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -1489,25 +1485,6 @@ def _migrate(conn_factory): CREATE INDEX IF NOT EXISTS idx_bj_user ON breeder_jahresberichte(user_id, jahr DESC); """) - # Partner-Codes (Influencer-Kooperationen + Gründer-Lizenz) - try: - conn.executescript(""" - CREATE TABLE IF NOT EXISTS partner_codes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - code TEXT NOT NULL UNIQUE COLLATE NOCASE, - label TEXT NOT NULL, - grants_founder INTEGER NOT NULL DEFAULT 1, - max_uses INTEGER DEFAULT NULL, - uses INTEGER NOT NULL DEFAULT 0, - created_by INTEGER REFERENCES users(id), - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_partner_codes_code ON partner_codes(code); - """) - logger.info("Migration: partner_codes Tabelle bereit.") - except Exception as e: - logger.warning(f"Migration partner_codes: {e}") - # js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()] if 'js_exercise_id' not in existing_te: diff --git a/backend/main.py b/backend/main.py index db883dc..fbae790 100644 --- a/backend/main.py +++ b/backend/main.py @@ -162,7 +162,6 @@ from routes.breeder_photos import router as breeder_photos_router from routes.zucht_hunde import router as zucht_hunde_router from routes.breeder_export import router as breeder_export_router from routes.zucht_ki import router as zucht_ki_router -from routes.partner import router as partner_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -194,7 +193,6 @@ app.include_router(breeder_photos_router, prefix="/api", tags=["Züchter- app.include_router(zucht_hunde_router, prefix="/api", tags=["Zuchtkartei"]) app.include_router(breeder_export_router, prefix="/api", tags=["Export"]) app.include_router(zucht_ki_router, prefix="/api", tags=["Züchter-KI"]) -app.include_router(partner_router, prefix="/api", tags=["Partner"]) app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"]) app.include_router(profile_router, prefix="/api/profile", tags=["Profil"]) app.include_router(import_router, prefix="/api/import", tags=["Import"]) @@ -693,26 +691,7 @@ async def favicon(): @app.get("/manifest.json") async def manifest(): - import json as _json - IS_STAGING = os.getenv("STAGING", "false").lower() == "true" - with open(f"{STATIC_DIR}/manifest.json", encoding="utf-8") as f: - data = _json.load(f) - if IS_STAGING: - data["name"] = "Ban Yaro Staging" - data["short_name"] = "BY ⚗️" - data["theme_color"] = "#7c3aed" - data["background_color"] = "#2d1b69" - data["id"] = "/staging" - # Icons mit Staging-Variante überschreiben falls vorhanden - staging_icon = "/icons/icon-192-staging.png" - import os as _os - if _os.path.exists(f"{STATIC_DIR}/icons/icon-192-staging.png"): - data["icons"] = [ - {"src": "/icons/icon-192-staging.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable"}, - {"src": "/icons/icon-512-staging.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}, - ] - from fastapi.responses import JSONResponse - return JSONResponse(content=data, media_type="application/manifest+json") + return FileResponse(f"{STATIC_DIR}/manifest.json") @app.get("/sw.js") async def service_worker(): @@ -1403,16 +1382,6 @@ async def knigge_page(): # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): - IS_STAGING = os.getenv("STAGING", "false").lower() == "true" - if IS_STAGING: - from fastapi.responses import HTMLResponse - with open(f"{STATIC_DIR}/index.html", encoding="utf-8") as f: - html = f.read() - html = html.replace( - '', - '', - ) - return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"}) return FileResponse( f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"} diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 32d5c6c..73349b4 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -78,42 +78,13 @@ async def register(data: RegisterRequest, response: Response, request: Request): new_user_id = user["id"] if data.ref_code: - code_upper = data.ref_code.strip().upper() - # Zuerst prüfen ob es ein Partner-Code ist - partner = conn.execute( - "SELECT id, grants_founder, max_uses, uses FROM partner_codes WHERE code=?", - (code_upper,) + referrer = conn.execute( + "SELECT id FROM users WHERE referral_code=? AND id != ?", + (data.ref_code.strip().upper(), new_user_id) ).fetchone() - if partner: - # Nur einlösen wenn max_uses nicht erreicht - if partner["max_uses"] is None or partner["uses"] < partner["max_uses"]: - conn.execute( - "UPDATE partner_codes SET uses=uses+1 WHERE id=?", - (partner["id"],) - ) - updates = {"referred_by": -partner["id"]} - if partner["grants_founder"]: - total_founders = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total_founders < 100: - founder_num = total_founders + 1 - updates["is_founder"] = 1 - updates["founder_number"] = founder_num - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {set_clause} WHERE id=?", - (*updates.values(), new_user_id) - ) - else: - # Fallback: Referral-Code eines anderen Users - referrer = conn.execute( - "SELECT id FROM users WHERE referral_code=? AND id != ?", - (code_upper, new_user_id) - ).fetchone() - if referrer: - conn.execute("UPDATE users SET referred_by=? WHERE id=?", - (referrer['id'], new_user_id)) + if referrer: + conn.execute("UPDATE users SET referred_by=? WHERE id=?", + (referrer['id'], new_user_id)) token = create_token(user["id"], user["rolle"]) _set_cookie(response, token) @@ -149,24 +120,6 @@ async def logout(response: Response): return {"ok": True} -_REFERRAL_TIERS = [ - (50, 50), - (20, 30), - (10, 20), -] - -def _referral_tier(count: int): - for threshold, discount in _REFERRAL_TIERS: - if count >= threshold: - return discount - return 0 - -def _referral_next(count: int): - for threshold, discount in reversed(_REFERRAL_TIERS): - if count < threshold: - return {"count": threshold, "discount": discount} - return None # Maximalstufe erreicht - @router.get("/referral") async def get_referral_info(user=Depends(get_current_user)): with db() as conn: @@ -180,14 +133,11 @@ async def get_referral_info(user=Depends(get_current_user)): if not code: code = _gen_referral_code() conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, user['id'])) - count = row["count"] if row else 0 - base = os.getenv("APP_URL", "https://banyaro.app") + base = os.getenv("APP_URL", "https://banyaro.app") return { - "code": code, - "count": count, - "link": f"{base}/?ref={code}", - "discount_pct": _referral_tier(count), - "next_tier": _referral_next(count), + "code": code, + "count": row["count"] if row else 0, + "link": f"{base}/?ref={code}", } diff --git a/backend/routes/forum.py b/backend/routes/forum.py index b6d204f..d22a460 100644 --- a/backend/routes/forum.py +++ b/backend/routes/forum.py @@ -131,7 +131,7 @@ async def list_threads( t.antworten, t.likes, t.views, t.is_pinned, t.is_locked, t.foto_urls, t.created_at, t.user_id, - u.name AS autor_name, u.founder_number AS autor_founder_number + u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.is_deleted = 0 @@ -183,7 +183,7 @@ async def create_thread(data: ThreadCreate, user=Depends(get_current_user)): data.thread_lat, data.thread_lon, data.thread_ort, ct) ) row = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ?""", @@ -203,7 +203,7 @@ async def get_thread(thread_id: int, user=Depends(get_current_user_optional)): uid = user['id'] if user else None with db() as conn: thread = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ? AND t.is_deleted = 0""", @@ -218,7 +218,7 @@ async def get_thread(thread_id: int, user=Depends(get_current_user_optional)): ) posts = conn.execute( - """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT p.*, u.name AS autor_name FROM forum_posts p LEFT JOIN users u ON u.id = p.user_id WHERE p.thread_id = ? @@ -288,7 +288,7 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre [*updates.values(), thread_id] ) row = conn.execute( - """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT t.*, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id = t.user_id WHERE t.id = ?""", @@ -328,7 +328,7 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current (thread_id,) ) row = conn.execute( - """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number + """SELECT p.*, u.name AS autor_name FROM forum_posts p LEFT JOIN users u ON u.id = p.user_id WHERE p.id = ?""", diff --git a/backend/routes/partner.py b/backend/routes/partner.py deleted file mode 100644 index 745fde7..0000000 --- a/backend/routes/partner.py +++ /dev/null @@ -1,192 +0,0 @@ -"""BAN YARO — Partner-Codes + Gründer-Lizenz""" - -from typing import Optional -from fastapi import APIRouter, HTTPException, Depends -from pydantic import BaseModel -from database import db -from auth import require_admin, get_current_user - -router = APIRouter() - - -class PartnerCodeCreate(BaseModel): - code: str - label: str - grants_founder: int = 1 - max_uses: Optional[int] = None - - -class GrantRequest(BaseModel): - is_founder: Optional[int] = None - is_partner: Optional[int] = None - - -# ------------------------------------------------------------------ -# Admin: Partner-Codes verwalten -# ------------------------------------------------------------------ - -@router.get("/admin/partner/codes") -def list_partner_codes(user=Depends(require_admin)): - """Alle Partner-Codes mit Stats (admin only).""" - with db() as conn: - rows = conn.execute( - """SELECT pc.id, pc.code, pc.label, pc.grants_founder, - pc.max_uses, pc.uses, pc.created_at, - u.name AS created_by_name - FROM partner_codes pc - LEFT JOIN users u ON u.id = pc.created_by - ORDER BY pc.created_at DESC""" - ).fetchall() - return [dict(r) for r in rows] - - -@router.post("/admin/partner/codes", status_code=201) -def create_partner_code(data: PartnerCodeCreate, user=Depends(require_admin)): - """Neuen Partner-Code erstellen (admin only).""" - code = data.code.strip().upper() - if not code: - raise HTTPException(400, "Code darf nicht leer sein.") - with db() as conn: - existing = conn.execute( - "SELECT id FROM partner_codes WHERE code=?", (code,) - ).fetchone() - if existing: - raise HTTPException(400, "Dieser Code existiert bereits.") - conn.execute( - """INSERT INTO partner_codes (code, label, grants_founder, max_uses, created_by) - VALUES (?, ?, ?, ?, ?)""", - (code, data.label.strip(), data.grants_founder, data.max_uses, user["id"]) - ) - row = conn.execute( - "SELECT * FROM partner_codes WHERE code=?", (code,) - ).fetchone() - return dict(row) - - -@router.delete("/admin/partner/codes/{code_id}", status_code=204) -def delete_partner_code(code_id: int, user=Depends(require_admin)): - """Partner-Code löschen (admin only).""" - with db() as conn: - existing = conn.execute( - "SELECT id FROM partner_codes WHERE id=?", (code_id,) - ).fetchone() - if not existing: - raise HTTPException(404, "Partner-Code nicht gefunden.") - conn.execute("DELETE FROM partner_codes WHERE id=?", (code_id,)) - return None - - -@router.post("/admin/partner/users/{user_id}/grant") -def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_admin)): - """Founder- und/oder Partner-Status für einen User setzen (admin only).""" - updates = {} - if data.is_founder is not None: - updates["is_founder"] = data.is_founder - if data.is_partner is not None: - updates["is_partner"] = data.is_partner - if not updates: - raise HTTPException(400, "Mindestens is_founder oder is_partner muss angegeben werden.") - with db() as conn: - target = conn.execute( - "SELECT id, is_founder, founder_number FROM users WHERE id=?", (user_id,) - ).fetchone() - if not target: - raise HTTPException(404, "User nicht gefunden.") - # Beim manuellen Vergeben von is_founder: founder_number zuweisen wenn noch keine - if updates.get("is_founder") == 1 and not target["founder_number"]: - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - if total >= FOUNDER_MAX: - raise HTTPException(400, f"Alle {FOUNDER_MAX} Gründer-Plätze sind vergeben.") - updates["founder_number"] = total + 1 - set_clause = ", ".join(f"{k}=?" for k in updates) - conn.execute( - f"UPDATE users SET {set_clause} WHERE id=?", - (*updates.values(), user_id) - ) - row = conn.execute( - "SELECT id, name, email, is_founder, is_partner, founder_number FROM users WHERE id=?", - (user_id,) - ).fetchone() - return dict(row) - - -@router.get("/admin/users/search") -def search_users(q: str, user=Depends(require_admin)): - """User-Suche für Admin (Name-Präfix, max. 10 Ergebnisse).""" - with db() as conn: - rows = conn.execute( - """SELECT id, name, email, is_founder, is_partner, rolle - FROM users WHERE name LIKE ? COLLATE NOCASE - ORDER BY name LIMIT 10""", - (f"{q}%",) - ).fetchall() - return [dict(r) for r in rows] - - -# ------------------------------------------------------------------ -# Öffentlich: Gründer-Leaderboard + Code-Info -# ------------------------------------------------------------------ - -FOUNDER_MAX = 100 - -@router.get("/partner/founders/stats") -def founders_stats(): - """Öffentliches Gründer-Leaderboard: Slots, Partner-Ranking, Gründer-Liste.""" - with db() as conn: - total = conn.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - # Partner-Ranking: nach uses absteigend - partners = conn.execute( - """SELECT pc.id, pc.label, pc.uses, pc.code, - u.name AS partner_username, u.is_partner - FROM partner_codes pc - LEFT JOIN users u ON u.referred_by = -pc.id AND u.is_partner=1 - WHERE pc.grants_founder=1 - ORDER BY pc.uses DESC""" - ).fetchall() - # Erste 100 Gründer (für Galerie) - founders = conn.execute( - """SELECT u.name, u.founder_number, - pc.label AS via_partner - FROM users u - LEFT JOIN partner_codes pc ON u.referred_by = -pc.id - WHERE u.is_founder=1 AND u.founder_number IS NOT NULL - ORDER BY u.founder_number ASC - LIMIT 100""" - ).fetchall() - return { - "total": total, - "max": FOUNDER_MAX, - "open": max(0, FOUNDER_MAX - total), - "closed": total >= FOUNDER_MAX, - "partners": [dict(p) for p in partners], - "founders": [dict(f) for f in founders], - } - - -@router.get("/partner/codes/{code}/info") -def partner_code_info(code: str): - """Gibt zurück ob ein Partner-Code existiert und dessen Label (öffentlich).""" - with db() as conn: - row = conn.execute( - """SELECT code, label, grants_founder, max_uses, uses - FROM partner_codes WHERE code=?""", - (code.strip().upper(),) - ).fetchone() - if not row: - raise HTTPException(404, "Partner-Code nicht gefunden.") - r = dict(row) - if r["grants_founder"]: - with db() as conn2: - total = conn2.execute( - "SELECT COUNT(*) FROM users WHERE is_founder=1" - ).fetchone()[0] - r["founder_slots_open"] = max(0, FOUNDER_MAX - total) - r["redeemable"] = (r["max_uses"] is None or r["uses"] < r["max_uses"]) and total < FOUNDER_MAX - else: - r["founder_slots_open"] = None - r["redeemable"] = r["max_uses"] is None or r["uses"] < r["max_uses"] - return r diff --git a/backend/static/css/layout.css b/backend/static/css/layout.css index 8ffa323..33fde8e 100644 --- a/backend/static/css/layout.css +++ b/backend/static/css/layout.css @@ -8,28 +8,25 @@ 1. APP SHELL ------------------------------------------------------------ */ #app { - display: flex; + display: flex; flex-direction: column; - min-height: 100dvh; + min-height: 100dvh; /* dvh: berücksichtigt mobile Browser-Chrome */ } /* Content-Bereich: füllt den Raum zwischen Header und Bottom-Nav */ #page-content { - flex: 1; - min-height: 0; /* iOS-Flex-Bug: ohne das scrollt body statt #page-content */ - overflow-y: auto; - overflow-x: hidden; + flex: 1; + overflow-y: auto; + overflow-x: hidden; padding-bottom: calc(var(--nav-bottom-height) + var(--safe-bottom) + 16px); - -webkit-overflow-scrolling: touch; } -/* Desktop: Sidebar-Layout — kein Bottom-Nav, natürliche Höhe */ +/* Desktop: Sidebar-Layout */ @media (min-width: 768px) { #app { flex-direction: row; } #page-content { - min-height: unset; padding-bottom: 0; padding-left: var(--nav-sidebar-width); } @@ -171,10 +168,6 @@ left: 0; right: 0; z-index: 700; /* über Leaflet-Panes (~400) */ - /* GPU-Layer erzwingen → iOS Safari fixed-position Stabilität */ - transform: translateZ(0); - -webkit-transform: translateZ(0); - will-change: transform; min-height: calc(var(--nav-bottom-height) + var(--safe-bottom)); padding-top: var(--space-1); padding-bottom: calc(var(--safe-bottom) + var(--space-1)); diff --git a/backend/static/icons/icon-180-staging.png b/backend/static/icons/icon-180-staging.png deleted file mode 100644 index fc2be5c..0000000 Binary files a/backend/static/icons/icon-180-staging.png and /dev/null differ diff --git a/backend/static/icons/icon-192-staging.png b/backend/static/icons/icon-192-staging.png deleted file mode 100644 index 9386e04..0000000 Binary files a/backend/static/icons/icon-192-staging.png and /dev/null differ diff --git a/backend/static/icons/icon-512-staging.png b/backend/static/icons/icon-512-staging.png deleted file mode 100644 index 0c2797a..0000000 Binary files a/backend/static/icons/icon-512-staging.png and /dev/null differ diff --git a/backend/static/index.html b/backend/static/index.html index 688dd83..82258db 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -88,12 +88,21 @@ - - - + + + + + +