feat(statistik): Aktivitäts-Tage je Nutzer erfassen (MAU/DAU-Grundlage)
Neue Tabelle user_activity_days (user_id, day, tenant_id, ein Eintrag je Nutzer und Kalendertag), angelegt per idempotenter Startup-Migration. Geschrieben wird sie in get_current_user bei jeder authentifizierten Nutzung, ein In-Memory-Tagesmerker begrenzt das auf einen INSERT OR IGNORE je Nutzer und Tag, Fehler werden geschluckt und blockieren nie eine Anfrage. Gelesen wird die Tabelle nur vom Verwaltungsportal für die neue MAU/DAU-Statistik. Ein reines Login-Protokoll wäre zu grob, weil das JWT 24 Stunden hält. Getestet gegen eine DB-Kopie, Tabelle entsteht, je Nutzer und Tag genau ein Eintrag trotz Mehrfachaufruf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
36
src/auth.py
36
src/auth.py
@@ -59,6 +59,41 @@ def decode_token(token: str) -> dict:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Aktivitaets-Erfassung fuer MAU/DAU ---------------------------------------
|
||||||
|
# Je Nutzer und Kalendertag genau ein Eintrag in user_activity_days. Der
|
||||||
|
# In-Memory-Merker haelt die DB-Last bei einem INSERT je Nutzer und Tag.
|
||||||
|
# Ein Fehler hier darf NIEMALS eine Anfrage blockieren.
|
||||||
|
_activity_seen: set = set()
|
||||||
|
|
||||||
|
|
||||||
|
async def _track_activity(user_id: int, tenant_id) -> None:
|
||||||
|
day = datetime.now(TIMEZONE).strftime("%Y-%m-%d")
|
||||||
|
key = (user_id, day)
|
||||||
|
if key in _activity_seen:
|
||||||
|
return
|
||||||
|
_activity_seen.add(key)
|
||||||
|
if len(_activity_seen) > 20000:
|
||||||
|
# Speicher begrenzen, alte Tage interessieren den Merker nicht mehr
|
||||||
|
_activity_seen.clear()
|
||||||
|
_activity_seen.add(key)
|
||||||
|
try:
|
||||||
|
import aiosqlite
|
||||||
|
from config import DB_PATH
|
||||||
|
db = await aiosqlite.connect(DB_PATH)
|
||||||
|
try:
|
||||||
|
await db.execute(
|
||||||
|
"INSERT OR IGNORE INTO user_activity_days (user_id, day, tenant_id) VALUES (?, ?, ?)",
|
||||||
|
(user_id, day, tenant_id),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
finally:
|
||||||
|
await db.close()
|
||||||
|
except Exception:
|
||||||
|
# Bewusst schlucken (z.B. Tabelle fehlt noch). Der Merker behaelt den
|
||||||
|
# Schluessel, damit ein Dauerfehler nicht jede Anfrage erneut trifft.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -69,6 +104,7 @@ async def get_current_user(
|
|||||||
detail="Nicht authentifiziert",
|
detail="Nicht authentifiziert",
|
||||||
)
|
)
|
||||||
payload = decode_token(credentials.credentials)
|
payload = decode_token(credentials.credentials)
|
||||||
|
await _track_activity(int(payload["sub"]), payload.get("tenant_id"))
|
||||||
return {
|
return {
|
||||||
"id": int(payload["sub"]),
|
"id": int(payload["sub"]),
|
||||||
"username": payload["username"],
|
"username": payload["username"],
|
||||||
|
|||||||
@@ -920,6 +920,24 @@ async def init_db():
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info("Migration: billing_tariff angelegt und mit Konfigurationswerten befuellt")
|
logger.info("Migration: billing_tariff angelegt und mit Konfigurationswerten befuellt")
|
||||||
|
|
||||||
|
# Migration: Aktivitaets-Tage je Nutzer fuer die MAU/DAU-Statistik im
|
||||||
|
# Verwaltungsportal. Geschrieben von auth.get_current_user (ein Eintrag
|
||||||
|
# je Nutzer und Tag), gelesen nur vom Portal.
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='user_activity_days'"
|
||||||
|
)
|
||||||
|
if not await cursor.fetchone():
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE user_activity_days (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
day TEXT NOT NULL,
|
||||||
|
tenant_id INTEGER,
|
||||||
|
PRIMARY KEY (user_id, day)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.commit()
|
||||||
|
logger.info("Migration: user_activity_days angelegt (MAU/DAU-Erfassung)")
|
||||||
|
|
||||||
# Migration: Token-Usage-Monatstabelle
|
# Migration: Token-Usage-Monatstabelle
|
||||||
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='token_usage_monthly'")
|
||||||
if not await cursor.fetchone():
|
if not await cursor.fetchone():
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren