Глобально уникальные ID (docs/changes/016): - у устройств, групп, задач, резервных копий и записей журнала ID вида <префикс>_<uuid7> (dev_, grp_, job_, bkp_, evt_): типы не пересекаются, внутри типа ID не повторяются и сортируются по времени; - миграция БД v1 заменяет числовые ID с пересчётом ссылок (копия файла БД перед миграцией, сверка числа строк, одна транзакция); - резервная копия = пара файлов с одним bkp_ ID, файлы в S3 получают метаданные backup-id/device-id; синхронизация метаданных с бакетом; - журнал событий в БД (events): создание/изменение/удаление, задачи, бэкапы, смена online/offline, вход в UI; API чтения GET /api/v1/events. Журнал в UI, ротация и очистка (docs/changes/017): - страница «Журнал»: фильтры, подгрузка «Показать ещё», окно записи; - настройки ротации (срок и максимум записей) хранятся в БД (схема v2), ротация при старте, раз в час и после сохранения настроек; - очистка журнала только через окно с паролем пользователя, блокировка после 5 неверных попыток, остаётся запись о факте очистки; через API очистки нет. Тесты: 20 из 20. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""Настройки, редактируемые из UI и хранимые в БД (app_settings); значения по умолчанию — из .env."""
|
|
import json
|
|
|
|
from app.config import get_settings
|
|
from app.db import session_scope
|
|
from app.models import AppSetting, now
|
|
from app.services import events
|
|
|
|
RETENTION_MAX_DAYS = 3650
|
|
ROWS_MIN, ROWS_MAX = 100, 10_000_000
|
|
|
|
|
|
def _get(key: str, default):
|
|
with session_scope() as s:
|
|
row = s.get(AppSetting, key)
|
|
return json.loads(row.value) if row is not None else default
|
|
|
|
|
|
def journal() -> dict:
|
|
"""Действующие настройки ротации: {'retention_days': int, 'max_rows': int}; 0 — без ограничения."""
|
|
cfg = get_settings()
|
|
return {"retention_days": _get("events.retention_days", cfg.events_retention_days),
|
|
"max_rows": _get("events.max_rows", cfg.events_max_rows)}
|
|
|
|
|
|
def validate_journal(retention_days, max_rows) -> dict:
|
|
try:
|
|
days, rows = int(str(retention_days).strip()), int(str(max_rows).strip())
|
|
except ValueError:
|
|
raise ValueError("Укажите целые числа") from None
|
|
if not 0 <= days <= RETENTION_MAX_DAYS:
|
|
raise ValueError(f"Срок хранения: целое число от 0 до {RETENTION_MAX_DAYS} (0 — без ограничения)")
|
|
if rows != 0 and not ROWS_MIN <= rows <= ROWS_MAX:
|
|
raise ValueError(f"Максимум записей: 0 (без ограничения) или от {ROWS_MIN} до {ROWS_MAX:,}".replace(",", " "))
|
|
return {"retention_days": days, "max_rows": rows}
|
|
|
|
|
|
def save_journal(retention_days, max_rows) -> dict:
|
|
"""Сохраняет настройки ротации, пишет событие и сразу применяет ротацию."""
|
|
new = validate_journal(retention_days, max_rows)
|
|
old = journal()
|
|
with session_scope() as s:
|
|
for key, value in (("events.retention_days", new["retention_days"]), ("events.max_rows", new["max_rows"])):
|
|
row = s.get(AppSetting, key)
|
|
if row is None:
|
|
s.add(AppSetting(key=key, value=json.dumps(value)))
|
|
else:
|
|
row.value, row.updated_at = json.dumps(value), now()
|
|
if new != old:
|
|
events.record("journal.settings_changed", "journal", None, "Изменены настройки ротации журнала",
|
|
data={"old": old, "new": new}, s=s)
|
|
events.rotate()
|
|
return new
|