Files
siftlode/backend/app/diagnostics.py
T
peter 6d8b7be6d8 feat!: remove the shared demo account
The demo account — one shared user that whitelisted emails entered from
the login page, reset on a schedule — goes end to end (owner decision,
E34): demo.py and the reset job, users.is_demo and demo_whitelist
(migration 0098 deletes the demo user, its audit rows, the reset job's
override and notices, then drops the column and table; the downgrade
restores the empty schema), the /auth/demo entry, the admin whitelist and
reset, the Demo tab, the banner, the login page's "Try the demo", and
every is_demo branch in routes, userscope, scheduler and the UI.

require_human only ever refused the demo account, so its 87 uses become
current_user (owner-approved); require_write_scope keeps its own check.
The login page's "or" divider now shows only beside the Google button:
without the demo entry under it, an instance without Google would have
said "or" to nothing.

A contract test (removedFeatures) fails on the word anywhere in the
backend, frontend, e2e or scripts — seen red on a planted hit. Size
ceilings tightened for the modules that shrank; admin.py is back under
the limit.
2026-10-05 00:19:38 +02:00

65 lines
2.8 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.
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)