"""A user's diagnostics level: what they consented to, and for how long. Three levels, named in the UI Alap / Részletes / Teljes: * INFO — the base, for everyone, no consent. The browser keeps its log in memory; nothing extra is sent or stored. * DEBUG — consented. The server logs this user's requests in detail and stores every line of them, and the browser streams its own log to `POST /api/diagnostics/events`. * TRACE — consented. As DEBUG, plus what the user typed into a search. A raised level always lapses after `ELEVATION_HOURS`: the expiry is computed HERE at consent time, never taken from the client, and an expired level reads as INFO without anything having to clear it. Core layer: imports `models` and `sysconfig`. """ from datetime import datetime, timedelta, timezone from typing import Literal, get_args from sqlalchemy.orm import Session from app import sysconfig from app.models import User DiagnosticsLevel = Literal["INFO", "DEBUG", "TRACE"] LEVELS: tuple[str, ...] = get_args(DiagnosticsLevel) BASE: DiagnosticsLevel = "INFO" ELEVATION_HOURS = 24 # How long a debug session records at most (app/debugsession.py). Here, beside the other # consent window, because the start dialog's consent text states it and reads it from `state`. SESSION_MINUTES = 60 def effective_level(user: User, now: datetime | None = None) -> DiagnosticsLevel: """The level in force for `user` right now: the consented one until it expires, else BASE.""" level, until = user.diagnostics_level, user.diagnostics_until if level not in LEVELS or level == BASE or until is None: return BASE if until <= (now or datetime.now(timezone.utc)): return BASE return level # type: ignore[return-value] — narrowed by the LEVELS check above def state(db: Session, user: User) -> dict: """The `/api/me` view of it: the level in force, when a raised one lapses, and how long stored log lines are kept — the consent text states that number, so it must be the instance's own.""" level = effective_level(user) return { "level": level, "until": user.diagnostics_until.isoformat() if level != BASE else None, "retention_days": sysconfig.get_int(db, "log_retention_days"), "session_minutes": SESSION_MINUTES, } def set_level(user: User, level: DiagnosticsLevel) -> None: """Record a consent (or its withdrawal). The caller commits. Raising the level again restarts the clock — each consent is for the next ELEVATION_HOURS, not an extension of an older one.""" if level == BASE: user.diagnostics_level = None user.diagnostics_until = None return user.diagnostics_level = level user.diagnostics_until = datetime.now(timezone.utc) + timedelta(hours=ELEVATION_HOURS)