@@ -0,0 +1,94 @@
|
||||
"""Журнал действий UI админки (JSONL): только события с клиента (setStatus). Сервер — в wesp.log, см. activity-feed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
_LOG = logging.getLogger(__name__)
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _activity_log_path(app) -> str:
|
||||
return (app.config.get("WESP_ADMIN_UI_ACTIVITY_LOG_PATH") or "").strip()
|
||||
|
||||
|
||||
def _line_unix_ts(obj: Dict[str, Any]) -> float | None:
|
||||
u = obj.get("u")
|
||||
if isinstance(u, (int, float)):
|
||||
return float(u)
|
||||
return None
|
||||
|
||||
|
||||
def append_admin_ui_activity(app, *, text: str, level: str, user_login: str) -> None:
|
||||
"""Дописывает одну строку JSON; строки старше WESP_ADMIN_UI_ACTIVITY_RETENTION_DAYS удаляются."""
|
||||
try:
|
||||
_append_admin_ui_activity_impl(app, text=text, level=level, user_login=user_login)
|
||||
except Exception:
|
||||
_LOG.exception("Сбой журнала UI-активности (WESP_ADMIN_UI_ACTIVITY_LOG_PATH).")
|
||||
|
||||
|
||||
def _append_admin_ui_activity_impl(app, *, text: str, level: str, user_login: str) -> None:
|
||||
path_str = _activity_log_path(app)
|
||||
if not path_str or app.config.get("TESTING"):
|
||||
return
|
||||
|
||||
path = Path(path_str)
|
||||
if not path.is_absolute():
|
||||
base = app.config.get("BASE_DIR", ".")
|
||||
path = Path(base) / path
|
||||
|
||||
row = {
|
||||
"u": int(time.time()),
|
||||
"level": level if level in ("ok", "err", "warn") else "ok",
|
||||
"user": (user_login or "")[:128],
|
||||
"text": (text or "")[:2000],
|
||||
}
|
||||
line = json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n"
|
||||
|
||||
days = int(app.config.get("WESP_ADMIN_UI_ACTIVITY_RETENTION_DAYS", 3) or 3)
|
||||
days = max(1, min(days, 30))
|
||||
cutoff = time.time() - float(days * 86400)
|
||||
|
||||
with _LOCK:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_rewrite_prune_and_append(path, cutoff, line)
|
||||
|
||||
|
||||
def _rewrite_prune_and_append(path: Path, cutoff_ts: float, new_line: str) -> None:
|
||||
lines_kept: List[str] = []
|
||||
if path.is_file():
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
for ln in f:
|
||||
raw = ln.strip()
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
ts = _line_unix_ts(obj)
|
||||
if ts is not None and ts >= cutoff_ts:
|
||||
lines_kept.append(raw + "\n")
|
||||
except OSError:
|
||||
lines_kept = []
|
||||
|
||||
tmp = path.with_name(f"{path.name}.tmp.{os.getpid()}")
|
||||
try:
|
||||
with tmp.open("w", encoding="utf-8") as out:
|
||||
for ln in lines_kept:
|
||||
out.write(ln)
|
||||
out.write(new_line)
|
||||
os.replace(str(tmp), str(path))
|
||||
except OSError:
|
||||
try:
|
||||
if tmp.is_file():
|
||||
tmp.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
Reference in New Issue
Block a user