diff --git a/.env.example b/.env.example index 1021443..f0fe315 100644 --- a/.env.example +++ b/.env.example @@ -48,4 +48,6 @@ TOOLBOX_VISIBLE="" # Чистый PDF (browserless на staging/prod; локально — SSH-туннель на staging) BROWSER_WS_URL="ws://localhost:3333" +# Секрет browserless (TOKEN в его env) — на прод/staging генерировать: openssl rand -hex 24 +BROWSERLESS_TOKEN="" PDF_MONTHLY_LIMIT="100" diff --git a/TECHNICAL.md b/TECHNICAL.md index a8d90cf..603aad6 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -171,6 +171,37 @@ CSS-классы: `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aub |---|---|---|---| | `POST` | `/api/auth/[...all]` | Better Auth handler | Все | | `POST` | `/api/admin/upload` | Загрузка файла в S3, возвращает `{ url, key }` | admin | +| `GET` | `/api/pdf` | Чистый PDF из URL (Bearer-ключ или сессия) — см. раздел ниже | платный студент, admin, curator | + +--- + +## Чистый PDF (`/tools/clean-pdf`, `/api/pdf`) + +Инструмент Obsidian Toolbox: превращает произвольный URL в чистый PDF (без рекламы и меню, с типографикой, оглавлением, A4/Letter, light/dark). Полный дизайн-документ: [`docs/specs/20260706-clean-pdf-design.md`](docs/specs/20260706-clean-pdf-design.md). + +**Пайплайн генерации:** + +``` +URL студента + → browserless (Chromium по WebSocket, playwright-core) загружает страницу + → Next.js забирает итоговый HTML + → Defuddle (JSDOM) выделяет основной контент + → HTML-шаблон (типографика, A4/Letter, light/dark, оглавление) + → Playwright page.pdf() в том же browserless + → application/pdf в ответе +``` + +Доступ: любой платный студент (есть `CourseEnrollment` вне `FREE_COURSE_SLUG`), admin/curator — без ограничений. Видимость страницы гейтится флагом `TOOLBOX_VISIBLE`, но сам `/api/pdf` работает независимо от него (доступ проверяется отдельно). Ключ для внешнего API — модель `PdfApiKey` (`sbpdf_`, ленивая генерация, регенерация инвалидирует старый). + +**Env-переменные:** + +| Переменная | Назначение | +|---|---| +| `BROWSER_WS_URL` | WebSocket-адрес browserless (`ws://browserless:3000` в compose, `ws://localhost:3333` при туннеле локально) | +| `BROWSERLESS_TOKEN` | Секрет browserless (`TOKEN` в его env) — общий и для сервиса, и для клиента в LMS | +| `PDF_MONTHLY_LIMIT` | Лимит генераций в месяц на студента (по умолчанию `100`), без пересборки | + +Контейнер `browserless` (`ghcr.io/browserless/chromium`) — внутренний, порт наружу не публикуется ни на staging, ни на проде. --- diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 1367c91..1baee72 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -13,9 +13,14 @@ services: NEXT_PUBLIC_APP_URL: "https://school.second-brain.ru" RESEND_API_KEY: "${RESEND_API_KEY}" EMAIL_FROM: "${EMAIL_FROM}" + BROWSER_WS_URL: "ws://browserless:3000" + BROWSERLESS_TOKEN: "${BROWSERLESS_TOKEN}" + PDF_MONTHLY_LIMIT: "${PDF_MONTHLY_LIMIT:-100}" depends_on: db: condition: service_healthy + browserless: + condition: service_started db: image: postgres:16-alpine @@ -32,5 +37,15 @@ services: timeout: 5s retries: 10 + browserless: + image: ghcr.io/browserless/chromium + restart: unless-stopped + environment: + TOKEN: "${BROWSERLESS_TOKEN}" + CONCURRENT: "2" + QUEUED: "10" + TIMEOUT: "120000" + mem_limit: 1g + volumes: postgres_data: