Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
"""BAN YARO — User-Profil Routes"""
|
|
|
|
import io
|
|
import os
|
|
import uuid
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
from pydantic import BaseModel
|
|
|
|
from auth import get_current_user
|
|
from database import db
|
|
|
|
router = APIRouter()
|
|
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|
|
|
VALID_ERFAHRUNG = {"einsteiger", "erfahren", "trainer", "zuechter"}
|
|
VALID_SICHTBARKEIT = {"public", "friends", "private"}
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
real_name: Optional[str] = None
|
|
bio: Optional[str] = None
|
|
wohnort: Optional[str] = None
|
|
erfahrung: Optional[str] = None
|
|
social_link: Optional[str] = None
|
|
profil_sichtbarkeit: Optional[str] = None
|
|
notes_ki_enabled: Optional[int] = None
|
|
|
|
|
|
def _load_user(user_id: int) -> dict:
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"""SELECT id, name, real_name, email, rolle, is_premium, email_verified,
|
|
bio, wohnort, erfahrung, social_link,
|
|
profil_sichtbarkeit, avatar_url, created_at
|
|
FROM users WHERE id=?""",
|
|
(user_id,)
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "User nicht gefunden.")
|
|
data = dict(row)
|
|
data["is_premium"] = bool(data["is_premium"])
|
|
return data
|
|
|
|
|
|
@router.patch("")
|
|
async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
|
|
fields = data.model_dump(exclude_none=True)
|
|
|
|
# Validierungen
|
|
if "erfahrung" in fields and fields["erfahrung"] not in VALID_ERFAHRUNG:
|
|
raise HTTPException(400, f"erfahrung muss eines von {sorted(VALID_ERFAHRUNG)} sein.")
|
|
if "profil_sichtbarkeit" in fields and fields["profil_sichtbarkeit"] not in VALID_SICHTBARKEIT:
|
|
raise HTTPException(400, f"profil_sichtbarkeit muss eines von {sorted(VALID_SICHTBARKEIT)} sein.")
|
|
if "bio" in fields and len(fields["bio"]) > 300:
|
|
raise HTTPException(400, "bio darf maximal 300 Zeichen lang sein.")
|
|
if "wohnort" in fields and len(fields["wohnort"]) > 60:
|
|
raise HTTPException(400, "wohnort darf maximal 60 Zeichen lang sein.")
|
|
if "social_link" in fields and len(fields["social_link"]) > 120:
|
|
raise HTTPException(400, "social_link darf maximal 120 Zeichen lang sein.")
|
|
|
|
if not fields:
|
|
return _load_user(user["id"])
|
|
|
|
set_clause = ", ".join(f"{k}=?" for k in fields)
|
|
values = list(fields.values()) + [user["id"]]
|
|
|
|
with db() as conn:
|
|
conn.execute(
|
|
f"UPDATE users SET {set_clause} WHERE id=?", values
|
|
)
|
|
|
|
return _load_user(user["id"])
|
|
|
|
|
|
@router.post("/avatar")
|
|
async def upload_avatar(
|
|
file: UploadFile = File(...),
|
|
user=Depends(get_current_user),
|
|
):
|
|
# HEIC-Support registrieren falls vorhanden
|
|
try:
|
|
import pillow_heif
|
|
pillow_heif.register_heif_opener()
|
|
except ImportError:
|
|
pass
|
|
|
|
from PIL import Image, ImageOps
|
|
|
|
content = await file.read()
|
|
try:
|
|
img = Image.open(io.BytesIO(content))
|
|
img = ImageOps.exif_transpose(img) # EXIF-Orientierung anwenden
|
|
img = img.convert("RGB")
|
|
buf = io.BytesIO()
|
|
img.save(buf, format="JPEG", quality=90)
|
|
content = buf.getvalue()
|
|
except Exception:
|
|
pass # Fallback: Originaldaten speichern
|
|
|
|
filename = f"avatar_{user['id']}_{uuid.uuid4().hex[:8]}.jpg"
|
|
path = os.path.join(MEDIA_DIR, "avatars", filename)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
with open(path, "wb") as f:
|
|
f.write(content)
|
|
|
|
avatar_url = f"/media/avatars/{filename}"
|
|
with db() as conn:
|
|
conn.execute(
|
|
"UPDATE users SET avatar_url=? WHERE id=?", (avatar_url, user["id"])
|
|
)
|
|
|
|
return {"avatar_url": avatar_url}
|