Хранилище админки и история действий

quite-admin управляет служебным хранилищем независимо от бизнес-моделей и ORM.
По умолчанию используется .quite-admin/state.sqlite3. Путь определяется относительно
рабочего каталога при создании Admin. Настройки, модели и миграции приложения не
нужны. При включённой истории файл создаётся лениво при первом авторизованном запросе.

SQLite по умолчанию

Встроенный SQLiteStore использует стандартную библиотеку Python. Соединение живёт
одну операцию в рабочем потоке и затем закрывается. Включён WAL, ожидание блокировки —
до 15 секунд. Новые каталоги и файлы создаются с закрытыми правами; существующие права
пользовательских файлов не меняются.

Нужен стабильный доступный для записи каталог. В контейнере подключите постоянный
volume к каталогу с файлом. Временный каталог не обеспечивает сохранность истории.
Несколько worker-процессов на одном сервере могут использовать один файл. Для разных
серверов используйте серверную БД; общий WAL-файл через NFS не поддерживается.
Для резервной копии работающей SQLite используйте её backup API либо остановите
запись перед созданием согласованной копии.

Необязательная переменная окружения позволяет обойтись без изменений Python-кода:

QUITE_ADMIN_STORAGE_URL=sqlite:////var/lib/my-app/admin.sqlite3

Явный аргумент storage имеет приоритет над окружением:

from quite_admin import Admin, SQLiteStore

admin = Admin(
    auth=auth,
    secret_key=session_secret,
    storage=SQLiteStore("/var/lib/my-app/admin.sqlite3", namespace="backoffice"),
)

Пользовательская SQLite, PostgreSQL или MySQL

Принимаются путь к SQLite-файлу/Path, URL sqlite:///..., серверный URL или экземпляр
хранилища. Для относительных SQLite-путей нужны три слеша, для абсолютных — четыре.
PostgreSQL/MySQL требуют quite-admin[sqlalchemy] и выбранный драйвер, например
psycopg[binary] или PyMySQL. Обычный SQLite-бэкенд их не требует.

admin = Admin(
    auth=auth,
    secret_key=session_secret,
    storage="postgresql+psycopg://user:password@localhost/application",
)
# Также можно задать через QUITE_ADMIN_STORAGE_URL:
# mysql+pymysql://user:password@localhost/application?charset=utf8mb4

Можно использовать существующий синхронный или асинхронный SQLAlchemy engine:

from quite_admin.stores.sqlalchemy import SQLAlchemyStore

admin = Admin(
    auth=auth,
    secret_key=session_secret,
    storage=SQLAlchemyStore(engine, namespace="backoffice"),
)

Жизненным циклом переданного engine управляет приложение. Служебные операции
используют собственные соединения и транзакции, не бизнес-сессию. Engine, созданный
по URL, по умолчанию использует NullPool и закрывает соединения после операций.
Настройки собственного пула можно передать прямо с URL:

storage = SQLAlchemyStore(
    database_url,
    namespace="backoffice",
    engine_options={"pool_size": 2, "max_overflow": 0, "pool_timeout": 3,
                    "pool_pre_ping": True},
)

engine_options принимает аргументы SQLAlchemy create_engine() и доступен только
с URL. Без явно заданного класса или размера пула используется NullPool.
Таймауты конкретного драйвера передаются через connect_args.
Методы подключения FastAPI и Litestar автоматически освобождают созданный по URL
движок storage при остановке. Для других интеграций есть await admin.aclose() и
async with admin.lifespan():. Переданный приложением engine не закрывается;
await storage.aclose() соблюдает то же правило владения. В обычном флоу mount
дополнительный shutdown-код не требуется.
Для асинхронного драйвера передайте AsyncEngine; строковые URL используют синхронные
драйверы. Все процессы/реплики должны использовать общее хранилище, namespace и
секрет сессии. Namespace разделяет данные разных админок в одной БД.

Служебные таблицы и формат документов

Библиотека автоматически создаёт таблицы и индексы с префиксом quiteadmin_:

Таблица Назначение
quiteadmin_meta Версия внутренней схемы хранилища
quiteadmin_documents Служебные JSON-документы с версиями
quiteadmin_activity Метаданные действий
quiteadmin_activity_details Старые и новые значения изменённых полей
quiteadmin_actors Компактный индекс пользователей по таблицам для подсказок

Префикс quiteadmin_ зарезервирован. Такие таблицы не появляются среди обычных
таблиц, в том числе при SQL discovery в той же БД. Для создания нужны DDL-права,
для обычной работы — чтение и запись. Инициализация синхронизирована между процессами.
Неизвестная версия схемы вызывает отказ, а не перезапись. Текущая версия — 2.
Библиотека управляет инициализацией и обновлением служебной схемы независимо от
миграций приложения, сохраняя документы и события. События только с метаданными
не содержат сравнений: ранее не записанные значения восстановить нельзя.

admin.storage предоставляет протокол AdminStore. Документ содержит ключ, тип,
владельца, JSON, версию формата и ревизию. save_document() создаёт документ с
expected_revision=0. Для изменения/удаления нужна прочитанная ревизия; конкурентное
изменение вызывает StorageConflict. JSON ограничен 1 МиБ. documents() возвращает
ограниченные страницы, ключ последнего документа передаётся в after. Это основа
для будущих дашбордов; редактор дашбордов в этот релиз не входит.

История действий

Обзор интерфейса показывает выбор пользователя, сравнение полей и
переключение Журнал/Таблица. Браузерное демо содержит вымышленные события
и локальные правки; ASGI-пример использует настоящее постоянное хранилище.

Журнал действий со сравнением сохранённых значений

Откройте История действий в полосе значков или среди инструментов на главной. Полоса использует тот же
компонент фильтров, что и листинг: Добавить фильтр открывает выбор пользователя,
таблицы, действия или результата. Значения можно искать, условия — редактировать
через чипы и удалять крестиком. Сбросить всё удаляет условия, сохраняя период:
его отдельный контрол находится в той же полосе. Пользователи берутся из истории,
включая удалённые аккаунты. Поиск по subject и отображаемому имени загружает
ограниченное число вариантов; выбор результата применяет точный subject. События идут
от новых к старым, время — UTC. Пагинация подписана, дополнительного COUNT нет.
Журнал сгруппирован по дням и по умолчанию показывает изменения. Выберите Все действия,
чтобы увидеть другие события, разрешённые history_level: экспорт и авторизация
требуют audit или all, просмотры — all. Раскройте событие для сравнения
Поле / Было / Стало или нажмите Развернуть все изменения. Из события можно
перейти к записи.
Переключатель Журнал / Таблица над событиями включает компактный табличный вид.
По умолчанию открыт журнал. В обоих видах доступно сравнение значений; переключение
сохраняет фильтры и текущую страницу. Выбранный вид остаётся в URL при применении
и сбросе фильтров, перезагрузке и переходе между страницами. JavaScript не требуется.

Компактное поле периода показывает применённые даты и UTC. По нажатию открывается
всплывающая панель: Быстрый выбор (сегодня, вчера, последние 7/30 дней, этот месяц,
любая дата), Конкретный день с одним календарём и От–до с двумя календарями
и редактируемыми границами. Быстрый период применяется сразу, выбранные вручную даты —
кнопкой Применить период. Отмена, Escape и нажатие вне панели сбрасывают
несохранённый выбор. Календари и вкладки поддерживают управление с клавиатуры.
День и обе границы диапазона включены целиком, от полуночи до последней микросекунды
UTC. Любую границу диапазона можно оставить пустой. Неактивные поля дат не влияют
на фильтрацию. Обратный диапазон и неверные даты отклоняются.
БД применяет период до пагинации с использованием индексов времени. При переключении
вида, очистке чипов и переходах между страницами период сохраняется. Для снятия
ограничения по дате выберите Любая дата в контроле периода.

Уровень записи

Подробность журнала задаётся одним необязательным аргументом конструктора:

admin = Admin(auth=auth, secret_key=session_secret, history_level="changes")
Уровень Какие события записываются и показываются
changes (по умолчанию) Завершённые создание, изменение, inline-правки, удаление и пользовательские действия над записью, включая изменение связей при сохранении.
audit Всё из changes, а также неуспешные записи/действия, вход/выход и экспорт.
all Всё из audit, а также списки и просмотры записей/форм, включая ошибки чтения.

Уровень проверяется до создания и добавления события, а не только в шаблоне.
Для исключённых запросов не записываются событие, значения полей и строка индекса
пользователей. Тот же уровень фильтрует старую историю и поиск пользователей до
пагинации. Настройка не удаляет данные: all может снова показать ранее записанные
события, но не восстановит те, которые не сохранялись. Настройка конструктора имеет
приоритет: параметры URL и Все действия не могут повысить уровень.

Прямые вызовы admin.storage по умолчанию возвращают все события:
настройки записи и отображения экземпляра админки к ним не применяются.

Ошибки авторизованных запросов включённого уровня содержат HTTP-статус. Анонимные
запросы и имена при неудачном входе не приписываются доверенному пользователю.
Просмотр истории и поиск пользователей не создают новых событий. Диагностические
логи приложения не зависят от этой настройки. Изменения вне quite-admin не фиксируются;
завершённая операция может не иметь видимых различий полей.

Встроенные SQLite- и SQLAlchemy-хранилища объявляют supports_history_levels=True
и принимают необязательный history_level в activities() / actors(), по умолчанию
all. SQL фильтрует данные до лимита и курсора. Обязательный протокол AdminStore
не меняется: для старых пользовательских хранилищ используются прежние фильтры и
чтение по страницам с отбором audit-событий и пользователей. Для больших историй
реализуйте необязательную возможность, чтобы фильтрация выполнялась эффективно.

Событие содержит время, subject и имя пользователя, slug таблицы, действие,
результат и HTTP-статус. Успешные изменения дополнительно содержат названия полей,
их старые и сохранённые значения, а также ключ записи, если его поля доступны.
При создании показано «отсутствует → значение», при удалении — наоборот. NULL,
пустая строка и false различаются. После пользовательского действия исходная запись
перечитывается из БД. Явно изменённые связи сравниваются по ключам связанных записей
в событии сохранения родителя. При ошибке сравнения сохранённых значений нет.

Скрытые и бинарные поля, распознаваемые пароли, токены и секреты исключены, даже если
чувствительное поле явно открыто в интерфейсе. Секреты во вложенном JSON скрываются.
При просмотре повторно проверяются текущая видимость полей и права на связи.
Лимиты: 4 КиБ на значение, 200 изменённых полей и 512 КиБ на событие; усечение явно
отмечается в интерфейсе. Тела форм, поисковые строки и SQL не сохраняются. Обычные
диагностические логи не содержат значений полей. Резервные копии хранилища содержат
исторические бизнес-данные: ограничивайте доступ к ним так же, как к исходным данным.

По умолчанию раздел доступен всем авторизованным администраторам. Страница истории
и поиск пользователей учитывают текущие права list и view на таблицы.
История удалённых/незарегистрированных таблиц скрыта. Авторизация показана отдельно.
Доступ ко всему разделу можно дополнительно ограничить:

admin = Admin(
    auth=auth,
    secret_key=session_secret,
    history_permission=lambda request, identity, action: "admin" in identity.roles,
)

Поддерживаются синхронные и асинхронные callbacks; передаётся action="view".

История включена по умолчанию (history_enabled=True). Полностью выключить функцию:

admin = Admin(auth=auth, secret_key=session_secret, history_enabled=False)

Раздел исчезает из меню, оба URL истории возвращают 404. События и значения полей
не собираются, автоматическая инициализация хранилища для истории не выполняется.
Существующая история сохраняется. admin.storage доступно независимо для других
служебных функций.

Срок хранения истории

Admin(history_retention_days=90) по умолчанию хранит историю 90 дней. Можно указать
другое положительное целое число (до 365000) или None для бессрочного хранения.
Настройка применяется к событиям и их деталям «до/после», независимо от history_level.
Бизнес-записи и служебные документы не удаляются.

При включённой истории первый запрос, инициализирующий storage, выполняет одну порцию
очистки: не больше 1000 старых событий. Последующие запросы запускают не больше одной
порции за пять минут на Admin/процесс. Планировщик и фоновый worker не требуются.
Очистка постепенная: без запросов она не запускается, а большой накопленный журнал
потребует нескольких порций. Несколько workers могут очищать историю одновременно;
каждая транзакция ограничена namespace. Вместе с событиями удаляются детали и записи
пользователей без оставшихся событий. Ошибка очистки даёт безопасное предупреждение
в логе и сохраняет обработку запроса; бизнес-операция не откатывается.
history_enabled=False отключает и автоматическую очистку.

Для немедленной обработки большого журнала используйте команду. По умолчанию она
удаляет события старше 90 дней порциями по 1000, повторяя транзакции до завершения.
Укажите те же storage и namespace, что в приложении. Без --storage используется
QUITE_ADMIN_STORAGE_URL; не помещайте URL с паролем в историю командной оболочки.

python -m quite_admin cleanup-history --storage /var/lib/my-app/admin.sqlite3 --namespace backoffice
python -m quite_admin cleanup-history --days 30 --batch-size 500

Из приложения можно выполнить одну порцию: await admin.purge_history(limit=1000).
Необязательный интерфейс HistoryRetentionStore.purge_activities(before=..., limit=1000)
принимает datetime с часовым поясом и возвращает число удалённых событий. Встроенные
SQLite и SQLAlchemy storage поддерживают его. Свой store остаётся совместимым с
AdminStore; для автоматической очистки реализуйте эту возможность атомарно,
включая детали и записи пользователей без событий. Без неё очистка пишет предупреждение,
а явный purge_history() выбрасывает NotImplementedError. Для хранения, сроком которого
управляет приложение, задайте history_retention_days=None. Прямые методы store
автоматическую очистку не запускают.

Ошибки и сохранность

При включённой истории ошибка инициализации хранилища возвращает безопасный 503 до входа или выполнения
защищённой операции. Запись события ожидается до отправки ответа. Последующая ошибка
служебного хранилища не может откатить уже сохранённое бизнес-изменение: ответ
сохраняется, а предупреждение quite_admin.activity_failed содержит только тип
исключения. Подмены постоянного хранилища памятью и фоновой очереди нет.

Бизнес-БД и служебное хранилище не участвуют в общей атомарной транзакции, даже при
одном engine. Сбой процесса/хранилища может оставить пропуск в истории. Для строгого
атомарного аудита нужен transactional outbox приложения или другая интеграция на
границе бизнес-транзакции. Изменения вне quite-admin не учитываются. События удаляются согласно настроенному сроку хранения; учитывайте его при
планировании диска и резервных копий.