diff --git a/backend/database.py b/backend/database.py index e71bd06..e428a56 100644 --- a/backend/database.py +++ b/backend/database.py @@ -560,8 +560,9 @@ def _migrate(conn_factory): ("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", "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: diff --git a/backend/routes/auth.py b/backend/routes/auth.py index a48ecb1..c0e39ce 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -93,7 +93,13 @@ async def register(data: RegisterRequest, response: Response, request: Request): ) updates = {"referred_by": -partner["id"]} if partner["grants_founder"]: - updates["is_founder"] = 1 + 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=?", diff --git a/backend/routes/forum.py b/backend/routes/forum.py index d22a460..b6d204f 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.name AS autor_name, u.founder_number AS autor_founder_number 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 + """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number 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 + """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number 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 + """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number 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 + """SELECT t.*, u.name AS autor_name, u.founder_number AS autor_founder_number 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 + """SELECT p.*, u.name AS autor_name, u.founder_number AS autor_founder_number 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 index 36d63c7..745fde7 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -87,16 +87,26 @@ def grant_user_status(user_id: int, data: GrantRequest, user=Depends(require_adm if not updates: raise HTTPException(400, "Mindestens is_founder oder is_partner muss angegeben werden.") with db() as conn: - target = conn.execute("SELECT id FROM users WHERE id=?", (user_id,)).fetchone() + 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 FROM users WHERE id=?", + "SELECT id, name, email, is_founder, is_partner, founder_number FROM users WHERE id=?", (user_id,) ).fetchone() return dict(row) @@ -116,9 +126,47 @@ def search_users(q: str, user=Depends(require_admin)): # ------------------------------------------------------------------ -# Öffentlich: Code-Info für Registrierungsseite +# Ö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).""" @@ -131,6 +179,14 @@ def partner_code_info(code: str): if not row: raise HTTPException(404, "Partner-Code nicht gefunden.") r = dict(row) - # Einlösbar? - r["redeemable"] = r["max_uses"] is None or r["uses"] < r["max_uses"] + 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/index.html b/backend/static/index.html index 3f4acbb..25a3ee5 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -230,6 +230,7 @@ border-top:1px solid var(--c-border,#e5e7eb); font-size:var(--text-xs);color:var(--c-text-muted); display:flex;gap:var(--space-3);padding-bottom:var(--space-2)"> + 🏆 100 Gründer Impressum Datenschutz @@ -419,6 +420,10 @@
+
+
+
+ diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b4cb681..bb05232 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 = '492'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '493'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.0.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; @@ -69,6 +69,7 @@ const App = (() => { wurfboerse: { title: 'Wurfbörse', module: null }, zuchthunde: { title: 'Zuchtkartei', module: null, requiresAuth: true }, 'zucht-profil': { title: 'Hunde-Profil', module: null }, + gruender: { title: '100 Gründer', module: null }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/pages/forum.js b/backend/static/js/pages/forum.js index c46fe6a..f29ff18 100644 --- a/backend/static/js/pages/forum.js +++ b/backend/static/js/pages/forum.js @@ -392,6 +392,7 @@ window.Page_forum = (() => {
${_esc(_initial(thread.autor_name))}
${_esc(thread.autor_name || 'Unbekannt')} + ${thread.autor_founder_number ? `Gründer #${thread.autor_founder_number}` : ''}