diff --git a/docs/superpowers/plans/20260706-clean-pdf.md b/docs/superpowers/plans/20260706-clean-pdf.md index 2efb138..73659b7 100644 --- a/docs/superpowers/plans/20260706-clean-pdf.md +++ b/docs/superpowers/plans/20260706-clean-pdf.md @@ -22,6 +22,7 @@ - Лимиты: `PDF_MONTHLY_LIMIT` (default 100), burst 5/мин, browserless `CONCURRENT=2`. - Проверки перед каждым коммитом: `npm run lint && npm run type-check && npm run test`. - Работать в ветке `feature/clean-pdf`. +- **Окружение:** на Mac НЕТ Docker. Контейнеры (postgres, browserless) живут на staging-сервере Hetzner: `root@178.104.27.196`, стек `/root/digital-household/lms-staging/` (docker-compose: `lms-staging-app-1` порт 3011 → https://staging.school.second-brain.ru, `lms-staging-db-1`). Сборочный клон репо на сервере: `/root/lms-staging-build/`. Деплой staging: `bash ~/Documents/Claude/scripts/deploy-staging.sh` (перед этим в `/root/lms-staging-build` должна быть выкачана нужная ветка). Миграции применяет entrypoint контейнера (`prisma migrate deploy`) при старте. Юнит-тесты, lint, type-check, build — локально на Mac; доступ к browserless с Mac — через SSH-туннель `ssh -f -N -L 3333:localhost:3333 root@178.104.27.196`. Все шаги плана с `docker compose ...` локально НЕ выполняются — их staging-эквиваленты прописаны в задачах. --- @@ -51,7 +52,7 @@ npm install defuddle jsdom playwright-core && npm install -D @types/jsdom Expected: package.json/package-lock.json обновлены, без ошибок peer-deps. -- [ ] **Step 3: browserless в dev-compose** +- [ ] **Step 3: browserless в dev-compose (на будущее, локально не проверяется)** В `docker-compose.yml` добавить сервис (после `db`, до `volumes`): @@ -67,13 +68,24 @@ Expected: package.json/package-lock.json обновлены, без ошибок TIMEOUT: "120000" ``` -- [ ] **Step 4: Поднять и проверить browserless** +На Mac Docker нет — этот файл правим для будущей локальной разработки, НЕ поднимаем. -```bash -docker compose up -d browserless && sleep 5 && curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3333/docs +- [ ] **Step 4: browserless в staging-стек на Hetzner** + +На сервере в `/root/digital-household/lms-staging/docker-compose.yml` добавить сервис `browserless` (тот же блок, что в Step 3 — с `ports: "127.0.0.1:3333:3000"`, порт нужен для SSH-туннеля с Mac), а в сервис `app` — `depends_on: browserless: condition: service_started` не добавлять (staging app перезапускается деплой-скриптом, зависимость не обязательна). В `/root/digital-household/lms-staging/.env` добавить: + +``` +BROWSER_WS_URL=ws://browserless:3000 +PDF_MONTHLY_LIMIT=100 ``` -Expected: `200`. Если имена env-переменных не подхватились (проверка: `docker compose logs browserless | head -30` — не должно быть предупреждений о неизвестных переменных), свериться с `http://localhost:3333/docs` и поправить. +Правку compose на сервере делать так: скачать файл (`scp`), поправить локально, залить обратно — НЕ heredoc в ssh. Затем: + +```bash +ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose up -d browserless && sleep 5 && curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3333/docs" +``` + +Expected: `200`. Проверить логи: `ssh root@178.104.27.196 "docker logs lms-staging-browserless-1 --tail 20"` — без ошибок конфигурации. - [ ] **Step 5: serverExternalPackages в next.config.ts** @@ -91,7 +103,7 @@ include: ["src/lib/**/__tests__/**/*.test.ts"], - [ ] **Step 7: env для dev** -В `.env.local` добавить: +В `.env.local` добавить (browserless доступен с Mac через SSH-туннель: `ssh -f -N -L 3333:localhost:3333 root@178.104.27.196`): ``` BROWSER_WS_URL=ws://localhost:3333 @@ -711,13 +723,36 @@ model PdfApiKey { pdfApiKey PdfApiKey? ``` -- [ ] **Step 2: Прогнать миграцию на dev-базе** +- [ ] **Step 2: Написать миграцию вручную и перегенерировать клиент** -```bash -docker compose up -d db && npx prisma migrate dev --name add_pdf_api_key +Локальной БД нет (`prisma migrate dev` невозможен) — миграцию пишем руками по образцу существующих (`prisma/migrations/20260623120000_add_tool_usage/migration.sql`), применит её `prisma migrate deploy` в entrypoint при деплое staging/prod. + +Создать `prisma/migrations/20260706120000_add_pdf_api_key/migration.sql`: + +```sql +-- Чистый PDF — персональный API-ключ студента (Zotero/curl) +CREATE TABLE "PdfApiKey" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "key" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "PdfApiKey_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "PdfApiKey_userId_key" ON "PdfApiKey"("userId"); +CREATE UNIQUE INDEX "PdfApiKey_key_key" ON "PdfApiKey"("key"); + +ALTER TABLE "PdfApiKey" ADD CONSTRAINT "PdfApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; ``` -Expected: миграция создана и применена, Prisma Client перегенерирован (`src/generated/prisma/`). +Затем перегенерировать клиент (БД не нужна): + +```bash +npx prisma generate +``` + +Expected: клиент в `src/generated/prisma/` обновился, `prisma.pdfApiKey` доступен в типах. - [ ] **Step 3: Написать keys.ts** @@ -1060,14 +1095,14 @@ describe.runIf(enabled)("generateCleanPdf (integration, нужен browserless)" }); ``` -- [ ] **Step 3: Run integration test** +- [ ] **Step 3: Run integration test (browserless на staging через туннель)** ```bash -docker compose up -d browserless +ssh -f -N -L 3333:localhost:3333 root@178.104.27.196 2>/dev/null || true RUN_PDF_INTEGRATION=1 BROWSER_WS_URL=ws://localhost:3333 npx vitest run src/lib/clean-pdf/__tests__/generate.int.test.ts ``` -Expected: PASS (~10–30 сек). Если URL статьи умер — заменить на любую живую статью с habr.com в тесте. +Expected: PASS (~10–40 сек с учётом туннеля). Если URL статьи умер — заменить на любую живую статью с habr.com в тесте. Если туннель не поднялся (`connection refused`) — проверить, что browserless на staging запущен (Task 1 Step 4). - [ ] **Step 4: Сохранить образец глазами** @@ -1204,35 +1239,42 @@ npm run lint && npm run type-check Expected: PASS. -- [ ] **Step 4: Живая проверка через dev-сервер** +- [ ] **Step 4: Живая проверка на staging** + +Запушить ветку и задеплоить её на staging: ```bash -docker compose up -d db browserless -npm run dev & -sleep 8 +git push -u origin feature/clean-pdf +ssh root@178.104.27.196 "cd /root/lms-staging-build && git fetch && git checkout feature/clean-pdf && git pull origin feature/clean-pdf" +bash ~/Documents/Claude/scripts/deploy-staging.sh +``` + +Expected: деплой зелёный (`✅ Staging OK`); в логах старта `Running database migrations...` без ошибок — миграция `add_pdf_api_key` применилась. + +```bash +BASE=https://staging.school.second-brain.ru # 1) Без авторизации → 401 -curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3000/api/pdf?url=https://example.com" +curl -s -o /dev/null -w "%{http_code}\n" "$BASE/api/pdf?url=https://example.com" # 2) С мусорным ключом → 401 -curl -s -H "Authorization: Bearer sbpdf_wrong" -o /dev/null -w "%{http_code}\n" "http://localhost:3000/api/pdf?url=https://example.com" +curl -s -H "Authorization: Bearer sbpdf_wrong" -o /dev/null -w "%{http_code}\n" "$BASE/api/pdf?url=https://example.com" ``` -Expected: `401` и `401`, оба JSON. Затем взять реальный ключ (после Task 5 создать вручную: +Expected: `401` и `401`, оба JSON. Затем вставить тестовый ключ админу в staging-БД: ```bash -docker compose exec db psql -U lms_user -d lms_db -c \ - "INSERT INTO \"PdfApiKey\" (id, \"userId\", key, \"createdAt\", \"updatedAt\") SELECT 'pdfkey-test-1', id, 'sbpdf_devtest123456789012345678901234', now(), now() FROM \"User\" WHERE role='admin' LIMIT 1;" +ssh root@178.104.27.196 "docker exec -i lms-staging-db-1 psql -U lms_staging_user -d lms_staging_db -c \"INSERT INTO \\\"PdfApiKey\\\" (id, \\\"userId\\\", key, \\\"createdAt\\\", \\\"updatedAt\\\") SELECT 'pdfkey-test-1', id, 'sbpdf_devtest123456789012345678901234', now(), now() FROM \\\"User\\\" WHERE role='admin' LIMIT 1;\"" curl -s -D - -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" \ - "http://localhost:3000/api/pdf?url=https://habr.com/ru/articles/942236/" -o /tmp/api-test.pdf | head -15 + "$BASE/api/pdf?url=https://habr.com/ru/articles/942236/" -o /tmp/api-test.pdf | head -15 ``` -Expected: `200`, `content-type: application/pdf`, заголовки `X-Uses-Count: 1`, `X-Max-Uses: 100`; `/tmp/api-test.pdf` открывается. Проверить SSRF: +Expected: `200`, `content-type: application/pdf`, заголовки `X-Uses-Count: 1`, `X-Max-Uses: 100`; `/tmp/api-test.pdf` открывается (`file /tmp/api-test.pdf` → PDF). Если экранирование psql через ssh мешает — положить SQL в файл и передать через stdin: `ssh root@178.104.27.196 "docker exec -i lms-staging-db-1 psql -U lms_staging_user -d lms_staging_db" < /tmp/insert-key.sql`. Проверить SSRF: ```bash -curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "http://localhost:3000/api/pdf?url=http://192.168.1.1/" ; echo -curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "http://localhost:3000/api/pdf?url=http://localhost:5432/"; echo +curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "$BASE/api/pdf?url=http://192.168.1.1/" ; echo +curl -s -H "Authorization: Bearer sbpdf_devtest123456789012345678901234" "$BASE/api/pdf?url=http://localhost:5432/"; echo ``` -Expected: оба `{"error":"Адрес недоступен"}` (422). Остановить dev-сервер. +Expected: оба `{"error":"Адрес недоступен"}` (422). - [ ] **Step 5: Commit** @@ -1530,13 +1572,11 @@ npm run lint && npm run type-check && npm run test Expected: PASS. -- [ ] **Step 6: Живая проверка в браузере** +- [ ] **Step 6: Живая проверка в браузере (staging)** -```bash -docker compose up -d db browserless && npm run dev -``` +Задеплоить текущее состояние ветки на staging (как в Task 8 Step 4: push → checkout ветки в `/root/lms-staging-build` → `deploy-staging.sh`). -Вручную (или через agent-browser в `--headed`): войти студентом с платным курсом → /tools → карточка «Чистый PDF» видна → открыть → сгенерировать PDF реальной статьи → файл скачался, счётчик увеличился → ключ показать/скопировать/перевыпустить (после перевыпуска старый ключ по curl даёт 401) → у аккаунта только с бесплатным курсом страница показывает заглушку, а /api/pdf отдаёт 403. +Через agent-browser в `--headed` (или вручную) на `https://staging.school.second-brain.ru`: войти админом (креды — memory `reference_lms_admin_credentials.md`; staging-БД содержит копию пользователей) → /tools → карточка «Чистый PDF» видна → открыть → сгенерировать PDF реальной статьи → файл скачался, счётчик увеличился → ключ показать/скопировать/перевыпустить (после перевыпуска старый ключ по curl даёт 401). Проверка заглушки для бесплатного аккаунта: найти в staging-БД пользователя без платных enrollment (или временно создать) и убедиться, что страница показывает заглушку, а /api/pdf с его сессией отдаёт 403. - [ ] **Step 7: Commit** @@ -1609,6 +1649,8 @@ git commit -m "Add browserless service to prod compose and document clean-pdf" ## Деплой-заметки (выполняются при релизе, не частью этого плана) +0. Staging уже получил browserless и env в Task 1 — на релизе речь только о проде (Hoster.kz). + 1. В `.env` на Hoster.kz добавить `BROWSERLESS_TOKEN=` (например `openssl rand -hex 24`) и при желании `PDF_MONTHLY_LIMIT`. 2. Миграция на проде: выполняется как обычно при деплое (`prisma migrate deploy` в entrypoint — проверить, что `add_pdf_api_key` применилась: `docker exec -i lms-sb-db-1 psql -U lms_user -d lms_db -c '\d "PdfApiKey"'`). 3. `docker pull ghcr.io/browserless/chromium` на Hoster.kz (интернет с сервера есть) + обновлённый compose.