The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
65 lines
2.8 KiB
Python
65 lines
2.8 KiB
Python
"""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)
|