diff --git a/docs/ai-log.md b/docs/ai-log.md index 0c3aba3..44b4df8 100644 --- a/docs/ai-log.md +++ b/docs/ai-log.md @@ -23,3 +23,4 @@ | 17 | Шаг 5, прод | `TRUSTED_PROXIES=172.31.120.1` не срабатывал: Node отдаёт адрес сокета как `::ffff:172.31.120.1`, и все посетители снова делили один лимит. Тесты этого не ловили — в них адреса без префикса | Проверка на проде: 6 запросов с X-Real-IP «A», затем «B» получил 429 | Нормализация IPv4-mapped адресов + тест; на сервере проверено пробным контейнером | | 18 | План, Telegram | План исходил из того, что VPS ходит в api.telegram.org напрямую; не проверил — с сервера в РФ Telegram недоступен (таймаут), бот не получил `/setup` | Тишина после `/setup` автора, затем проверка `fetch` из контейнера | Ретранслятор Bot API на домашнем сервере (`deploy/tg-relay`) по Tailscale + `TELEGRAM_API_BASE` | | 19 | Шаг 9, бот | Сетевые ошибки `getUpdates` молча повторялись без единой строки в логе — недоступность Telegram была невидима | Пустой `docker logs` при неработающем боте | Предупреждение в лог раз в 10 минут | +| 20 | Оркестрация | Оркестратор закоммитил ретранслятор через `git add -A` в том же чекауте, где дизайнер вёл незакоммиченную вёрстку шага 12 — почти весь шаг 12 уехал в коммит `20db25b` «ретранслятор Bot API», а на прод выкатилась половина вёрстки | Отчёт дизайнера | Впредь — `git add` по путям и отдельный worktree для параллельных агентов | diff --git a/package.json b/package.json index a6a17ff..c67aebd 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "private": true, "type": "module", "packageManager": "pnpm@11.22.0", - "engines": { "node": ">=22" }, + "engines": { + "node": ">=22" + }, "scripts": { "dev": "pnpm -r --parallel dev", "build": "pnpm -r build", @@ -13,6 +15,7 @@ }, "devDependencies": { "@types/node": "^26.6.5", + "playwright": "^1.64.1", "typescript": "~6.0.3", "vitest": "^5.0.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42a5026..8ddf367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@types/node': specifier: ^26.6.5 version: 26.6.5 + playwright: + specifier: ^1.64.1 + version: 1.64.1 typescript: specifier: ~6.0.3 version: 6.0.3 @@ -2343,6 +2346,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.64.1: + resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.64.1: + resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==} + engines: {node: '>=20'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4969,6 +4982,12 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + playwright-core@1.64.1: {} + + playwright@1.64.1: + dependencies: + playwright-core: 1.64.1 + possible-typed-array-names@1.1.0: {} postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.30)(tsx@4.23.15): diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs new file mode 100644 index 0000000..2cc7a6f --- /dev/null +++ b/scripts/screenshots.mjs @@ -0,0 +1,29 @@ +// Скриншоты для критика: полная страница и первый экран на 390 и 1440. +// node scripts/screenshots.mjs <каталог> +// Анимации доигрываются: страница прокручивается до низа, чтобы сработали IntersectionObserver. +import { mkdir } from 'node:fs/promises'; +import { chromium } from 'playwright'; + +const [url = 'http://127.0.0.1:3000/', outDir = '.screenshots/critic'] = process.argv.slice(2); +await mkdir(outDir, { recursive: true }); + +const browser = await chromium.launch(); +for (const width of [390, 1440]) { + const page = await browser.newPage({ + viewport: { width, height: width === 390 ? 844 : 900 }, + deviceScaleFactor: width === 390 ? 2 : 1, + }); + await page.goto(url, { waitUntil: 'networkidle' }); + await page.screenshot({ path: `${outDir}/${width}-fold.png` }); + for (let y = 0; y < (await page.evaluate(() => document.body.scrollHeight)); y += 400) { + await page.evaluate((top) => window.scrollTo(0, top), y); + await page.waitForTimeout(120); + } + await page.waitForTimeout(800); + await page.evaluate(() => window.scrollTo(0, 0)); + await page.screenshot({ path: `${outDir}/${width}-full.png`, fullPage: true }); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); + console.log(`${width}: ${outDir}/${width}-full.png, горизонтальный выход за экран ${overflow}px`); + await page.close(); +} +await browser.close();