Feature+Fix: Referral-Admin, Pro-Gates, Karten-Layer, onDogChange, Staging-Media (SW by-v855)
Features: - Admin: Referral-Tab (Virality Factor, Top-Werber, letzte Einladungen) - Karte: Regenradar (RainViewer, zoom→7, color=4), Temperatur-Layer (OWM) mit Zahlen-Grid + Legende - Wetter-Chip: Umschwung-Warnung bei ≥40%-Sprung in Niederschlagswahrscheinlichkeit - Freundschaftsanfragen: Accept/Decline direkt in Notifications (kein Pro nötig) - Freunde-Seite für Standard-User freigeschaltet Pro-Gates: - KI-Trainer, Routenvorschläge, Regenradar, Temperatur-Layer jetzt Pro-Feature - Pro-Badge (P) auf Chips für Admins/Mods in allen Welten + Welten-einrichten - Oranger Banner auf Pro-Seiten für Admin/Mod/Manager Bugfixes: - onDogChange: uebungen.js (Cache leeren + _render), trainingsplaene.js (war leer) - robots.txt vereinfacht (nur Disallow, kein Allow-Durcheinander) - Hintergrund-Foto: Querformat-Filter korrigiert (kein Fallback auf Hochformat) - Staging Media: FileResponse mit korrektem MIME-Type, no-cache statt immutable - Staging Docker: MEDIA_DIR=/data/media + /prod-media:ro Fallback-Handler - Staging-Fix: Bild-Upload auf zweitem Hund (war Read-only file system)
This commit is contained in:
parent
2f021f54c2
commit
79fa5684b9
22 changed files with 570 additions and 58 deletions
|
|
@ -583,6 +583,48 @@ async def scheduler_trigger(job_id: str, user=Depends(require_admin)):
|
|||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/admin/referrals — User-wirbt-User Top 100
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/referrals")
|
||||
async def referral_stats(user=Depends(require_mod)):
|
||||
with db() as conn:
|
||||
# Top-Werber mit Anzahl
|
||||
top = conn.execute("""
|
||||
SELECT r.id, r.name, r.email,
|
||||
COUNT(u.id) AS invited_count,
|
||||
r.created_at AS member_since
|
||||
FROM users u
|
||||
JOIN users r ON r.id = u.referred_by
|
||||
GROUP BY r.id
|
||||
ORDER BY invited_count DESC
|
||||
LIMIT 100
|
||||
""").fetchall()
|
||||
|
||||
# Alle Einladungen (für Detail-Ansicht)
|
||||
invites = conn.execute("""
|
||||
SELECT u.id, u.name, u.email, u.created_at,
|
||||
r.id AS referrer_id, r.name AS referrer_name
|
||||
FROM users u
|
||||
JOIN users r ON r.id = u.referred_by
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT 500
|
||||
""").fetchall()
|
||||
|
||||
total_users = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
total_referred = conn.execute(
|
||||
"SELECT COUNT(*) FROM users WHERE referred_by IS NOT NULL"
|
||||
).fetchone()[0]
|
||||
|
||||
return {
|
||||
"top_referrers": [dict(r) for r in top],
|
||||
"recent_invites": [dict(r) for r in invites],
|
||||
"total_users": total_users,
|
||||
"total_referred": total_referred,
|
||||
"viral_factor": round(total_referred / max(total_users - total_referred, 1), 2),
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/admin/ki/history — 30-Tage-Verlauf + Top-User (all-time)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -188,8 +188,7 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
# Zufälliges Foto aus den letzten 100 Tagebuchbildern
|
||||
# Alle Querformat-Fotos (breiter als hoch) des Hundes, stabile Reihenfolge
|
||||
# Hintergrundfoto: Querformat-Bilder bevorzugt, tagesweise rotierend
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id = dm.diary_id
|
||||
|
|
@ -198,12 +197,13 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
# Fallback: alle Fotos ohne Maß-Filter (Bilder vor dem Backfill)
|
||||
# Fallback: Bilder ohne Dimensionsdaten (vor dem Backfill hochgeladen)
|
||||
if not photos:
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id = dm.diary_id
|
||||
WHERE d.dog_id=? AND dm.media_type='image'
|
||||
AND dm.img_width IS NULL
|
||||
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
|
|
|
|||
|
|
@ -172,6 +172,20 @@ async def send_request(target_id: int, user=Depends(get_current_user)):
|
|||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/pending")
|
||||
async def pending_requests(user=Depends(get_current_user)):
|
||||
"""Eingehende Freundschaftsanfragen — kein Pro nötig, für Notification-Accept."""
|
||||
with db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT f.id, u.name AS requester_name, u.avatar_url
|
||||
FROM friendships f
|
||||
JOIN users u ON u.id = f.requester_id
|
||||
WHERE f.addressee_id=? AND f.status='pending'
|
||||
ORDER BY f.created_at DESC
|
||||
""", (user["id"],)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/{friendship_id}/accept")
|
||||
async def accept_request(friendship_id: int, user=Depends(get_current_user)):
|
||||
uid = user["id"]
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Schreibe klar und strukturiert, ohne unnötigen Fachjargon."""
|
|||
prompt=prompt,
|
||||
system=system,
|
||||
max_tokens=600,
|
||||
requires_premium=False,
|
||||
requires_premium=True,
|
||||
user_id=user["id"],
|
||||
)
|
||||
return {"antwort": result}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,10 @@ async def suggest_route(data: SuggestRequest, user=Depends(get_current_user)):
|
|||
if not ORS_API_KEY:
|
||||
raise HTTPException(503, "ORS nicht konfiguriert")
|
||||
|
||||
from auth import has_pro_access
|
||||
if not has_pro_access(user):
|
||||
raise HTTPException(403, "Routenvorschläge sind ein Pro-Feature.")
|
||||
|
||||
is_privileged = (
|
||||
user.get("rolle") in ("admin", "moderator") or
|
||||
user.get("is_moderator") or
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ BAN YARO — Wetter-API
|
|||
GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from fastapi import APIRouter, Query, HTTPException, Depends
|
||||
import weather as weather_module
|
||||
|
|
@ -11,6 +12,34 @@ from database import db
|
|||
|
||||
router = APIRouter()
|
||||
|
||||
OWM_API_KEY = os.getenv("OPENWEATHERMAP_KEY", "")
|
||||
|
||||
|
||||
ALLOWED_OWM_LAYERS = {"temp_new", "clouds_new", "wind_new", "pressure_new", "precipitation_new"}
|
||||
|
||||
|
||||
@router.get('/radar-tiles')
|
||||
async def radar_tile_config(user=Depends(get_current_user)):
|
||||
"""Regenradar-Tile-Config (RainViewer)."""
|
||||
return {"provider": "rainviewer"}
|
||||
|
||||
|
||||
@router.get('/layer-tiles')
|
||||
async def layer_tile_config(
|
||||
layer: str = "temp_new",
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
"""OWM-Tile-URL für Wetter-Layer (Key bleibt server-seitig)."""
|
||||
if layer not in ALLOWED_OWM_LAYERS:
|
||||
raise HTTPException(400, f"Unbekannter Layer. Erlaubt: {', '.join(ALLOWED_OWM_LAYERS)}")
|
||||
if not OWM_API_KEY:
|
||||
raise HTTPException(503, "OWM nicht konfiguriert.")
|
||||
return {
|
||||
"url": f"https://tile.openweathermap.org/map/{layer}/{{z}}/{{x}}/{{y}}.png?appid={OWM_API_KEY}",
|
||||
"maxNativeZoom": 18,
|
||||
"opacity": 0.6,
|
||||
}
|
||||
|
||||
|
||||
@router.get('')
|
||||
async def get_weather(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue