BOOK-LIBRARY: архитектура

BOOK-LIBRARY — это не одна страница с перечнем файлов, а несколько связанных приложений вокруг одного backend. Публичная SvelteKit-витрина отвечает за спокойный путь читателя к книге, React-админка — за операционную работу, а backend на Express держит API, SQLite, uploads, очереди, сессии, временные download-токены и production-hosted bundle админки.

Архитектура здесь нужна не ради схемы на бумаге. Она отделяет чтение от обслуживания библиотеки: читатель не видит админского шума, а администратор получает место, где можно импортировать книги, проверять внешние источники, запускать переводы, разбираться с провайдерами, следить за безопасностью и понимать статусы задач. Backend остаётся точкой, где принимаются решения о данных, файлах и долгих процессах.

Главная идея этой статьи — показать, почему BOOK-LIBRARY устроен слоями. Каталог, импорт, переводы, публикация и сопровождение требуют разных интерфейсов и разной степени доверия к данным. Если смешать всё в одну поверхность, библиотека быстро станет хрупкой; если развести роли аккуратно, проект проще развивать и обслуживать.

Основные части

В архитектуре есть четыре заметных слоя:

  • Express backend — API, SQLite, uploads, sessions, jobs, download, security и legacy routes;
  • SvelteKit storefront — публичная витрина каталога для читателя;
  • React admin — основная админка и операционный центр;
  • legacy EJS UI — простой HTML-интерфейс внутри backend.

Старая Svelte-admin часть удалена. Это важное изменение: SvelteKit остаётся публичной витриной, а админские задачи больше не конкурируют между двумя frontend-слоями.

Express backend

Backend — это рабочая часть системы, куда сходятся и читательские запросы, и админские операции. Он обслуживает несколько типов маршрутов:

  • public API для категорий, книг, переводов и download-token запроса;
  • admin API для управления каталогом, импортом, переводами, провайдерами и настройками;
  • download routes для выдачи файлов по временным токенам;
  • legacy routes для EJS-интерфейса;
  • production-hosted React admin bundle, если backend развёрнут вместе с админкой.

На backend также лежит работа, которую нельзя честно переложить на клиент: проверка сессий, CSRF для state-changing admin API, CORS-разделение public/admin origins, доступ к SQLite, работа с uploads и запуск durable jobs. Клиенты остаются интерфейсами, а backend держит правила, состояние и последствия операций.

SQLite и группы данных

SQLite остаётся хорошим выбором для частной библиотеки: его проще разворачивать, бэкапить и переносить вместе с uploads. Но по мере роста проекта база перестаёт быть просто «списком книг». В ней появляется память о том, что происходило с каталогом, импортами, переводами и доступом.

Условно данные делятся на несколько групп:

  • каталог: книги, категории, связи с деревом категорий, метаданные и обложки;
  • доступ: admin_users, SQLite-backed sessions, смена пароля и emergency env override;
  • скачивание: временные токены и сроки их жизни;
  • импорт: jobs, статусы, результаты, ошибки, отчёты и повторы;
  • внешние источники: данные поиска и импорта из Anna’s Archive и Flibusta;
  • LLM: runtime-провайдеры, priority, fallback и настройки;
  • переводы: translation jobs, segments, QA findings, artifacts, publish/unpublish состояние.

Ценность такой схемы не в количестве таблиц, а в том, что долгие и спорные процессы получают собственное состояние. Импорт или перевод можно показать в админке, повторить, отменить, разобрать по ошибкам — и не превращать каждый сбой в ручное расследование по файлам и логам.

Дерево категорий

Категории стали деревом, а не плоским списком. Для публичной витрины это меняет навигацию: теперь есть breadcrumbs, подкатегории и subtree counts. Читатель может идти от крупной темы к более узкой, а не угадывать нужный раздел в длинном списке без структуры.

Для API это тоже важно. Книги внутри категории отдаются постранично через limit и offset, вместе с hasMore и total. Такой ответ лучше подходит большим разделам. При этом старый формат не сломан: legacy-клиенты могут запросить format=legacy или all=1.

Публичная SvelteKit-витрина

Storefront отвечает за читательский опыт:

  • показать дерево категорий;
  • вывести breadcrumbs и подкатегории;
  • загрузить книги выбранной категории постранично;
  • показать доступные опубликованные переводы;
  • запросить временную ссылку для скачивания.

Витрина не должна знать внутренности очередей, провайдеров или админских настроек. Её задача проще и важнее: не мешать человеку найти книгу и скачать доступный файл. Поэтому она работает через public API и получает только то, что можно показать читателю.

React-админка

React-админка стала целевой поверхностью для управления проектом. В ней собраны dashboard, операции, поиск, переводы, настройки, пользователи, смена пароля, провайдеры и security-сценарии. Это место не для читателя, а для человека, который поддерживает библиотеку в порядке.

Админка использует более подходящий для операционного интерфейса стек: React, shadcn/ui-подход к компонентам и тёмную нейтральную палитру. Важнее не визуальная эффектность, а то, чтобы задачи, ошибки и настройки были видны без раскапывания логов.

Отдельная роль React-админки — быть UI для долгих процессов. Импорт архива, поиск во внешнем источнике или LLM-перевод не обязаны укладываться в один запрос. Админка показывает состояние durable jobs через polling и превращает длинные операции в наблюдаемый рабочий процесс.

Durable jobs и polling

Долгие операции вынесены в устойчивые задачи:

  • одиночный импорт файла;
  • batch ZIP import;
  • поиск и импорт из Anna’s Archive;
  • поиск и импорт из Flibusta;
  • LLM translation jobs;
  • повтор неудачных этапов и отмена задач.

Раньше пакетный импорт можно было воспринимать как потоковый ответ. Сейчас главный акцент другой: задача живёт в базе, имеет статус, прогресс, события и ошибки. Клиент может закрыться, обновиться или потерять соединение, а backend всё равно сохраняет текущее состояние операции.

Polling здесь выглядит уместнее, чем попытка держать длинный запрос до конца. Для админки важнее предсказуемость: видеть, что задача queued, running, failed, cancelled или completed, и понимать, что делать дальше. Так импорт и переводы становятся не «чёрным ящиком», а процессом, за которым можно следить и который можно чинить.

Временные токены скачивания

Книги не отдаются по постоянному публичному пути. Читатель или Quartz-интеграция сначала запрашивает временный token, а backend выдаёт файл через download route. Токен живёт ограниченное время и может быть очищен после истечения срока.

Это не превращает систему в защищённый DRM, но убирает прямую раздачу стабильных URL на файлы. Для частной библиотеки это важный слой аккуратности: публичная витрина остаётся удобной, а файловое хранилище не раскрывается как обычная папка со статическими ссылками.

Безопасность

Security-слой заметно продвинулся по сравнению с ранним вариантом:

  • сессии администратора хранятся в SQLite;
  • есть таблица admin_users;
  • пароль можно менять из админки;
  • emergency env override оставлен для восстановления доступа;
  • state-changing admin API защищены CSRF;
  • CORS разделён для public и admin origins;
  • public CORP настроен для обложек;
  • backend может обслуживать production React admin bundle.

Важно не переоценивать это как «абсолютную безопасность». Файловые загрузки, внешние источники, архивы, OCR и генерация PDF всё равно требуют осторожности. Но базовый слой админского доступа, cookies, origins и CSRF уже не выглядит как черновой прототип.

Production pipeline

Production pipeline разворачивает backend вместе с hosted React admin. Публичная SvelteKit-витрина выкатывается отдельно, если она нужна как отдельная поверхность. Такое разделение удобно: backend и админка составляют операционную часть, а storefront можно обновлять или размещать отдельно.

Для Quartz-интеграции это тоже полезно. Quartz не должен становиться backend для книг. Он может брать generated catalog и mirrored covers, а операции с файлами и download-токенами остаются в BOOK-LIBRARY. В итоге публикация каталога не смешивается с обслуживанием файлов, очередей и переводов.

Legacy EJS UI

Legacy EJS UI остаётся как простой HTML-слой. Его смысл не в том, чтобы конкурировать с SvelteKit или React, а в запасном варианте доступа и в более прямом интерфейсе без отдельной frontend-сборки.

Для проекта это нормальный компромисс: основной путь развивается в сторону SvelteKit storefront и React admin, но backend всё ещё может отдать базовую HTML-страницу. Такой слой полезен именно своей простотой: он не делает систему красивее, зато снижает зависимость от полноценной frontend-сборки.

Текущие ограничения

Несмотря на заметное развитие, у архитектуры есть честные ограничения:

  • SQLite требует дисциплины бэкапов вместе с uploads;
  • OCR и production PDF renderer для переводов остаются deferred scope;
  • DOCX/PDF media extraction, translation memory и budget dashboards ещё не закрыты;
  • внешние источники зависят от доступности и качества ответа;
  • публичная витрина показывает только published translations, поэтому черновики нужно проверять в админке;
  • сложные роли пользователей пока не являются главным сценарием.

Эти ограничения не противоречат текущей стадии проекта. BOOK-LIBRARY уже выглядит как рабочая библиотечная система, а не как набор форм вокруг SQLite. Главное, что архитектура задаёт понятные границы: где читатель видит каталог, где администратор разбирает операции, где backend отвечает за файлы и состояние. Следующий рост логично вести через качество данных, наблюдаемость, бэкапы и аккуратное расширение переводческого pipeline.