Files
lms-sb/docs/specs/20260706-clean-pdf-design.md
T
2026-07-06 10:10:11 +05:00

9.6 KiB
Raw Blame History

Чистый PDF — инструмент «URL → PDF» в LMS

Дата: 20260706 Статус: дизайн утверждается Референс: Clean PDF (pdf.brainysnipe.ru) из поста https://t.me/flowing_abyss/94, инструкция https://flowing-abyss.com/Clean-PDF-for-Zotero

Что делаем

Инструмент для студентов школы: превращает любую веб-страницу в чистый PDF — без рекламы, меню и мусора, с нормальной типографикой, оглавлением, форматами A4/Letter и светлой/тёмной темой. Два сценария плюс открытый API:

  1. Веб-форма в кабинете (/tools/clean-pdf): вставил URL — скачал PDF.
  2. Zotero: персональный API-ключ + скрипт для плагина Actions & Tags — правый клик на записи → PDF генерируется и прикрепляется к ней.
  3. Открытый API для продвинутых: curl с Bearer-ключом, документация на странице инструмента.

Принятые решения

Вопрос Решение
Архитектура Всё внутри LMS (подход B): страница и API в Next.js, рендер — отдельный контейнер browserless в том же compose
Кому доступ Любой платный студент: есть CourseEnrollment на курс со slug ≠ FREE_COURSE_SLUG; admin/curator — без ограничений
Видимость Часть Obsidian Toolbox, живёт под существующим флагом TOOLBOX_VISIBLE; включение на проде — отдельное решение позже
Лимит 100 генераций/месяц на студента, env PDF_MONTHLY_LIMIT (без пересборки), плюс burst-лимит 5/мин

Пайплайн генерации

URL студента
  → Chromium (browserless) загружает страницу (JS отрабатывает)
  → Next.js забирает итоговый HTML
  → Defuddle (npm, через JSDOM) выделяет главный контент
  → чистый HTML-шаблон: типографика, A4/Letter, light/dark, оглавление из заголовков
  → Playwright page.pdf() в том же browserless
  → файл application/pdf в ответ
  • Defuddle — библиотека kepano (автор Obsidian), та же, что у референса: https://github.com/kepano/defuddle
  • browserless/chromium — отдельный контейнер в docker-compose: MAX_CONCURRENT_SESSIONS=2, встроенная очередь, mem_limit ~1 ГБ, порт наружу не публикуется.
  • Next.js подключается по WebSocket через playwright-core (только клиент, Chromium в образ LMS не попадает — образ не растёт).
  • Таймаут всего пайплайна ~90 сек; понятные JSON-ошибки: URL недоступен, лимит исчерпан, нет платного доступа.

Данные (Prisma)

Новая модель:

model PdfApiKey {
  id        String    @id @default(cuid())
  userId    String    @unique
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  key       String    @unique   // формат sbpdf_<random>, показывается студенту
  createdAt DateTime  @default(now())
}
  • Ключ создаётся лениво при первом заходе на страницу инструмента.
  • Перегенерация заменяет ключ (старый перестаёт работать сразу).
  • Учёт использований — существующая ToolUsage с tool = "clean-pdf-generate"; месячный счётчик = COUNT за календарный месяц. Именно отдельный идентификатор: CopyButton на странице логирует копирования как tool = "clean-pdf", и они не должны тратить лимит генераций.
  • Проверка платного доступа выполняется на каждый запрос → рефанд или блокировка аккаунта автоматически отключает и веб, и ключ. Источник истины один — БД LMS.

API

GET /api/pdf?url=<...>&format=A4|Letter&theme=light|dark

  • Авторизация, два пути: заголовок Authorization: Bearer sbpdf_... (Zotero, curl) или активная сессия Better Auth (веб-форма — студенту ключ для веба не нужен).
  • Роут добавляется в PUBLIC_ROUTES в middleware.ts (иначе 307 → /login — известные грабли).
  • Ответ: application/pdf + заголовки X-Uses-Count / X-Max-Uses (их же показывает Zotero-скрипт).
  • Ошибки: JSON { error } c честными статусами (401 нет ключа/сессии, 403 нет платного доступа, 429 лимит, 422 плохой URL, 504 не отрендерилось).

Страница /tools/clean-pdf

По образцу существующих инструментов тулбокса (реестр TOOLS, ToolCard, страница + клиентская форма):

  1. Форма: поле URL, выбор формата и темы, кнопка «Скачать PDF», счётчик «использовано N из 100 в этом месяце».
  2. Блок «Zotero»: персональный ключ (показать/скопировать/перегенерировать), пошаговая инструкция по Actions & Tags, готовый скрипт с уже подставленными адресом школы и ключом студента (адаптация скрипта референса: SERVICE_URL = https://school.second-brain.ru, endpoint /api/pdf).
  3. Блок «API»: пример curl.

Дизайн — строго ДС-2 (перед вёрсткой перечитать SBT/02-Стандарты/Дизайн-LMS/DESIGN.md).

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

Сервис скачивает произвольные URL изнутри прод-стенда — главный риск SSRF:

  • Только http:// и https://.
  • Перед загрузкой — резолв DNS и блокировка приватных/зарезервированных диапазонов: localhost/127.x, 10.x, 172.1631.x, 192.168.x, 169.254.x (метаданные облаков), ::1, fc00::/7, плюс docker-хостнеймы стенда (db, app, browserless).
  • Внутри browserless — перехват сетевых запросов страницы с той же фильтрацией (защита от редиректов и подгрузок на внутренние адреса).
  • Порт browserless наружу не публикуется, доступен только приложению по внутренней сети compose.
  • Куки/учётные данные пользователя на целевую страницу не передаются.
  • Санитайз имени файла в Content-Disposition.
  • Лимиты: 100/мес (env) + burst 5/мин на пользователя; сверху — очередь browserless (2 конкурентных рендера).

Деплой

  • docker-compose.yml (dev) и docker-compose.prod.yml: сервис browserless (образ ghcr.io/browserless/chromium), внутренняя сеть, лимиты памяти.
  • env: BROWSER_WS_URL, PDF_MONTHLY_LIMIT=100, существующий TOOLBOX_VISIBLE.
  • Prisma-миграция (PdfApiKey).
  • Стандартная схема деплоя LMS: сборка на Hetzner → docker save | ssh | docker load на Hoster.kz; browserless на Hoster.kz — обычный docker pull.
  • Hot-standby на Hetzner получает тот же compose (репликация БД уже покрывает PdfApiKey и ToolUsage).

Тестирование

  • Юнит: SSRF-валидатор (таблица адресов → допуск/блок), пайплайн Defuddle → HTML-шаблон на фикстурах.
  • Интеграция на staging: рендер 3–5 реальных статей (лонгрид, статья с картинками, JS-тяжёлая страница), проверка лимитов и обоих путей авторизации.
  • Zotero-скрипт — ручная проверка на живой библиотеке.
  • Веб-форма — e2e через agent-browser в --headed (headless режет антиспам — известные грабли).

Вне рамок (YAGNI)

  • Отдельный домен pdf.second-brain.ru — не нужен, всё на school.second-brain.ru.
  • Хранение сгенерированных PDF на сервере — файл отдаётся сразу и не сохраняется.
  • Пер-курсовый гейтинг, тарифные лимиты, платные квоты — не сейчас.
  • Освещение в рассылке/анонсы — после включения TOOLBOX_VISIBLE на проде, отдельной задачей.