feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции. - Учётные записи: завести, сменить роль и службу, заблокировать, сбросить пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль и проверка «это твой разбор». - Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия классификатора, число живых занятий. Отдельной строкой — секрет сессии: значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть её должен администратор, а не проверяющий на защите. - Журнал действий с фильтром, только на чтение. - Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный, а если его нет — из контейнера базы; если нет ни того ни другого, администратор видит, чего не хватает, а кнопка не молчит. Границы роли выражены отсутствием маршрутов, а не проверками внутри них: в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста проверяют это перебором маршрутов роутера — так запрет нельзя ослабить, случайно добавив обработчик. Найдено по ходу: тесты с базой падали в компании и проходили поодиночке. Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура, дающая движок на тест. 193 теста зелёных (12 новых).
This commit is contained in:
parent
d55c8bc8cd
commit
641e505c67
13 changed files with 948 additions and 1 deletions
42
backend/scripts/backup.py
Normal file
42
backend/scripts/backup.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""make backup: снять резервную копию базы.
|
||||
|
||||
Та же операция, что кнопка на АРМ администратора, — но вызываемая из cron:
|
||||
ТЗ требует копирования не реже раза в сутки, а полагаться на то, что кто-то
|
||||
нажмёт кнопку, нельзя.
|
||||
|
||||
make backup снять копию
|
||||
0 3 * * * cd … && make backup в crontab демо-машины
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.admin.backup import BackupError, create, listing # noqa: E402
|
||||
|
||||
#: Сколько копий держать. Место на демо-машине не бесконечно, а копия за
|
||||
#: позапрошлую неделю не нужна никому.
|
||||
KEEP = 14
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
created = create()
|
||||
except BackupError as exc:
|
||||
print(f"копия не снята: {exc}")
|
||||
return 1
|
||||
|
||||
print(f"{created['name']}: {created['size_bytes'] // 1024} КБ")
|
||||
|
||||
old = listing()[KEEP:]
|
||||
for item in old:
|
||||
(ROOT / "backups" / item["name"]).unlink(missing_ok=True)
|
||||
if old:
|
||||
print(f"удалено старых копий: {len(old)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in a new issue