Perf: 9 Performance-Fixes — SW by-v1072

Backend:
- DB: 3 neue Indizes (forum_posts thread+user, routes user) — Forum/Routen-Queries
- Caching: cache.py (TTL-Cache ohne neue Dependency) für 5 statische Listen
  (training_exercises, pflege_tipps, wiki_stats, wiki_gruppen, help_articles)
- diary.py + breeder_photos.py: Bildverarbeitung (ffmpeg/PIL/EXIF) per
  run_in_executor → blockiert Event-Loop nicht mehr
- scheduler.py: 11 kollidierende Jobs auf 5-Min-Intervalle gestaggert, coalesce=True
- social.py: ORDER BY RANDOM() ohne LIMIT in 2 Stellen gefixt
- alerts.py: Haversine-Loop bekommt SQL-Bounding-Box-Vorfilter

Frontend:
- sw.js: Tile-Cache mit LRU-Eviction (max 500 Einträge)
- admin.js: Event-Listener-Leak — Tab-Klicks per Delegation statt N Listener
- api.js: compressImage() Helper — Client-seitiges Resize auf max 2000px
  (HEIC/Videos/<500KB unverändert), integriert in 8 Upload-Stellen
  (diary, dog-profile×2, walks, poison, lost, health×2)

Bump APP_VER 1071 → 1072 (sw.js, app.js, main.py, index.html)
This commit is contained in:
rene 2026-05-26 06:30:36 +02:00
parent 3abf974d29
commit c03884cb81
23 changed files with 461 additions and 120 deletions

View file

@ -1,6 +1,6 @@
"""BAN YARO — Tagebuch Routes"""
import os, uuid, json, math, logging
import os, uuid, json, math, logging, asyncio
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
@ -684,7 +684,13 @@ async def upload_media(dog_id: int, entry_id: int,
validate_upload(raw_data, file.filename or "")
except ValueError as e:
raise HTTPException(415, str(e))
raw_data, ext = convert_media(raw_data, file.filename or "")
# Blockierende Bild-/Video-Konvertierung in Threadpool auslagern,
# damit der Event-Loop für andere Requests frei bleibt.
loop = asyncio.get_event_loop()
raw_data, ext = await loop.run_in_executor(
None, lambda: convert_media(raw_data, file.filename or "")
)
if not ext:
ext = ".jpg"
filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
@ -692,17 +698,21 @@ async def upload_media(dog_id: int, entry_id: int,
media_type = _guess_media_type(ct, file.filename or "")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(raw_data)
def _write_bytes(p: str, data: bytes) -> None:
with open(p, "wb") as f:
f.write(data)
await loop.run_in_executor(None, lambda: _write_bytes(path, raw_data))
if media_type == "video":
extract_video_thumb(path)
await loop.run_in_executor(None, lambda: extract_video_thumb(path))
elif media_type == "image":
preview_bytes = generate_preview(raw_data, ext)
preview_bytes = await loop.run_in_executor(
None, lambda: generate_preview(raw_data, ext)
)
if preview_bytes:
preview_path = os.path.splitext(path)[0] + "_preview.webp"
with open(preview_path, "wb") as f:
f.write(preview_bytes)
await loop.run_in_executor(None, lambda: _write_bytes(preview_path, preview_bytes))
media_url = f"/media/diary/{filename}"
@ -710,8 +720,8 @@ async def upload_media(dog_id: int, entry_id: int,
exif_gps = None
img_size = None
if media_type == "image":
exif_gps = extract_gps_from_exif(raw_data)
img_size = get_image_size(raw_data)
exif_gps = await loop.run_in_executor(None, lambda: extract_gps_from_exif(raw_data))
img_size = await loop.run_in_executor(None, lambda: get_image_size(raw_data))
with db() as conn:
# sort_order = nächste freie Position