- the start dialog is the consent: level (Részletes/Teljes), what is recorded, retention and the cap (now served as diagnostics.session_minutes) - the pill docks under the toasts: elapsed time, Step (an optional label, its own Escape layer), Screenshot (tab capture where the browser has it), Stop; compact on a phone - while recording, lib/log tags every event and every request path (req, beacon, uploadReq via one identityHeaders) names the session; applyDiagnostics raises the level for the session's run and never lowers a raised one, and lapses at whichever ends first - Stop sends what is waiting before the server closes the session; the cap says so and opens the composer - Settings → Diagnosztika and the command palette start/step/stop it - the evidence leaves the log upload's own request lines out - the step shortcut is deliberately palette-only: the keymap's D1 rule bars a new modifier chord, and a bare letter collides with the players'
66 lines
2.9 KiB
Python
66 lines
2.9 KiB
Python
"""A user's diagnostics level (E22 S5): 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.
|
|
The shared demo account cannot raise its level — its log would mix every visitor's session.
|
|
|
|
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 (E16, 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)
|