feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
"""make backup: снять резервную копию базы.
|
|
|
|
|
|
|
|
|
|
|
|
Та же операция, что кнопка на АРМ администратора, — но вызываемая из cron:
|
|
|
|
|
|
ТЗ требует копирования не реже раза в сутки, а полагаться на то, что кто-то
|
|
|
|
|
|
нажмёт кнопку, нельзя.
|
|
|
|
|
|
|
|
|
|
|
|
make backup снять копию
|
|
|
|
|
|
0 3 * * * cd … && make backup в crontab демо-машины
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
import os
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
#: Сколько копий держать. Место на демо-машине не бесконечно, а копия за
|
|
|
|
|
|
#: позапрошлую неделю не нужна никому.
|
2026-09-24 01:10:49 +03:00
|
|
|
|
KEEP = max(1, int(os.environ.get("BACKUP_KEEP", "14")))
|
feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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())
|