wa-dev-tools/DOCS.md

1255 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WA Dev Tools — Полная документация проекта
> Версия документации: март 2026
> Репозиторий: `git.wadevelop.ru/treamz/wa-dev-tools`
> Продакшн: `https://wadevelop.ru`
---
## Содержание
1. [Обзор проекта](#1-обзор-проекта)
2. [Архитектура](#2-архитектура)
3. [Инструменты](#3-инструменты)
4. [Авторизация и роли](#4-авторизация-и-роли)
5. [AdminJS панель](#5-adminjs-панель)
6. [База данных](#6-база-данных)
7. [API Reference](#7-api-reference)
8. [Фронтенд архитектура](#8-фронтенд-архитектура)
9. [Безопасность](#9-безопасность)
10. [Деплой и инфраструктура](#10-деплой-и-инфраструктура)
11. [Мониторинг](#11-мониторинг)
12. [Известные ограничения](#12-известные-ограничения)
13. [Будущие доработки](#13-будущие-доработки)
---
## 1. Обзор проекта
**WA Dev Tools** — веб-платформа с набором утилит для разработчиков. Доступна по адресу `https://wadevelop.ru`. Целевая аудитория: веб-разработчики, которым нужны быстрые инструменты для работы с изображениями, видео, PDF, кодом и HTTP-запросами без установки десктопных приложений.
### Технологии
| Слой | Технология |
|------|-----------|
| Среда выполнения | Node.js 20 |
| Веб-фреймворк | Express 5 |
| База данных | MariaDB 10.11 |
| Хранение сессий | express-mysql-session (MariaDB) |
| Очередь задач | better-sqlite3 (SQLite WAL, `/mnt/webdata/storage/jobs.db`) |
| Обработка изображений | Sharp 0.34 |
| Обработка видео | FFmpeg 5.1.8 (libx264, libvpx, libmp3lame), ffprobe |
| Обработка PDF | pdf-lib, pdf-parse, Ghostscript (системный) |
| Парсинг HTML | @mozilla/readability, jsdom |
| Авторизация | express-session, bcrypt |
| Панель администратора | AdminJS 7 + @adminjs/sequelize |
| Стили | Tailwind CSS 3 (CLI build, `vendor/tailwind.min.css`) |
| WebSocket | ws (реалтайм прогресс конвертации) |
| Логирование | pino (структурированный JSON) |
| Процесс-менеджер | PM2 |
### Сервер
- Raspberry Pi 5, 8 GB RAM, Debian Bookworm
- Nginx как реверс-прокси с SSL от Let's Encrypt
- Файлы проекта: `/mnt/webdata/www/images.wadevelop.ru/`
---
## 2. Архитектура
### Файловая структура
```
/mnt/webdata/www/images.wadevelop.ru/
├── server.js # Точка входа: Express app, middleware pipeline, запуск сервера
├── package.json # Зависимости (name: "tinypng" — историческое название)
├── ecosystem.config.js # PM2 конфигурация
├── .env # Переменные окружения (не в git)
├── compress.log # Лог операций сжатия (пишется через lib/logger.logToFile)
├── lib/
│ ├── auth.js # Middleware авторизации: список публичных путей, редиректы
│ ├── admin.js # Настройка AdminJS: Sequelize модели, ресурсы, монтирование
│ ├── db.js # Пул соединений MariaDB (mysql2/promise), хелперы для users
│ ├── logger.js # Логгер pino: структурированный JSON + logToFile
│ ├── queue.js # Очередь задач на SQLite (better-sqlite3, WAL mode)
│ ├── session.js # Persistent sessions (express-mysql-session → MariaDB)
│ ├── storage.js # Единые пути хранения: /mnt/webdata/storage/{uploads,results}
│ ├── ws.js # WebSocket сервер (ws) — реалтайм прогресс конвертации
│ └── ssrf.js # Защита от SSRF: проверка IP-диапазонов, DNS-резолвинг
├── routes/
│ ├── api.js # Публичный API: /api/settings, /api/tools, /api/content/:section
│ ├── auth.js # Авторизация: login, register, logout, /auth/me
│ ├── compress.js # Конвертер изображений: сжатие, ресайз, архивирование
│ ├── video.js # Видео конвертер: загрузка, конвертация через FFmpeg, прогресс
│ ├── pdf.js # PDF инструменты: merge, split, rotate, watermark и др.
│ ├── parser.js # Парсер статей: Readability + jsdom, кэш, несколько эндпоинтов
│ ├── httpclient.js # HTTP-клиент: прокси-запросы, история в сессии
│ ├── redirects.js # Redirect-анализатор: пошаговый обход цепочки редиректов
│ ├── placeholder.js # Placeholder-изображения: генерация через Sharp + SVG
│ ├── svgeditor.js # SVG редактор: страница + оптимизация + AI-иконки (lookup)
│ ├── status.js # Страница статуса: проверка DB, сервисов, uptime
│ ├── pages.js # Статические страницы: landing, dashboard, md, editor и др.
│ └── logs.js # [вспомогательный] логирование (не экспортирует маршруты)
├── public/
│ ├── shared.css # Все общие стили: переменные, sidebar, top-header, компоненты
│ ├── shared.js # Tailwind config, WA_TOOLS, WA_CATEGORIES, sidebar, тема
│ ├── landing.html # Лендинг (публичный)
│ ├── dashboard.html # Главная для авторизованных
│ ├── login.html # Форма входа
│ ├── register.html # Форма регистрации
│ ├── status.html # Страница статуса сервисов
│ ├── compress.html # Инструмент: конвертер изображений
│ ├── video.html # Инструмент: видео конвертер
│ ├── pdf.html # Инструмент: PDF инструменты
│ ├── parser.html # Инструмент: парсер статей
│ ├── httpclient.html # Инструмент: HTTP-клиент
│ ├── redirects.html # Инструмент: redirect-анализатор
│ ├── placeholder.html # Инструмент: placeholder изображения
│ ├── svgeditor.html # Инструмент: SVG редактор (full-screen)
│ ├── editor.html # Инструмент: фоторедактор (full-screen)
│ ├── formatter.html # Инструмент: code formatter (только фронтенд)
│ ├── sanitizer.html # Инструмент: HTML sanitizer (только фронтенд)
│ ├── converter.html # Инструмент: format converter (только фронтенд)
│ ├── password.html # Инструмент: генератор паролей (только фронтенд)
│ ├── md.html # Инструмент: markdown viewer (только фронтенд)
│ └── vendor/ # Локальные копии: tailwind.js, fonts.css
├── uploads/ # Временные загруженные файлы (автоочистка 10 мин)
└── downloads/ # Результаты обработки (автоочистка 30 мин)
```
### Middleware pipeline
Порядок регистрации middleware в `server.js` критичен для корректной работы авторизации:
```
Запрос
├─ helmet() # Security headers (CSP отключен)
├─ session() # express-session: cookie maxAge 24ч
├─ express.static('public') # Статика — всегда публична
├─ /auth/* # Auth routes — до auth middleware
├─ GET /api/settings # Публичные API для лендинга
├─ GET /api/tools #
├─ GET /api/content/advantages #
├─ GET /api/content/dashboard #
├─ /status/* # Статус — публичный
├─ authMiddleware() # ← ГРАНИЦА АВТОРИЗАЦИИ
├─ /api/* # Защищённый API
├─ /health # Health check
├─ /compress # Инструменты (защищены)
├─ /download/:filename # Скачивание файлов (защищено)
├─ /parser, /placeholder # ...
├─ /httpclient, /redirects # ...
├─ /svgeditor, /video, /pdf # ...
├─ /admin/* # AdminJS (доп. проверка role=admin)
└─ pages router # Страницы: /, /dashboard, /md и т.д.
```
### Как работает авторизация
`lib/auth.js` реализует middleware-функцию, которая пропускает запросы по белому списку без проверки:
**Публичные пути** (без авторизации):
- `PUBLIC_PATHS`: `/`, `/health`, `/favicon.ico`, `/status`, `/placeholder`
- `PUBLIC_API`: `/api/settings`, `/api/tools`, `/api/content/advantages`, `/api/content/dashboard`
- `PUBLIC_FILES`: `/shared.css`, `/shared.js`, `/landing.html`
- `PUBLIC_PREFIXES`: `/auth/`, `/vendor/`, `/placeholder-img/`, `/status/`
Всё остальное требует `req.session.user`. При отсутствии сессии:
- GET-запросы — редирект на `/auth/login` с сохранением `returnTo`
- API-запросы — `401 JSON`
### Как работает AdminJS
AdminJS v7 — ESM-only пакет. Поскольку основное приложение использует CommonJS (`"type": "commonjs"`), AdminJS загружается через динамический импорт внутри async IIFE в `server.js`:
```js
const { setupAdmin } = require('./lib/admin');
await setupAdmin(app);
```
Функция `setupAdmin` в `lib/admin.js`:
1. Создаёт отдельный экземпляр Sequelize (параллельно с пулом из `lib/db.js`)
2. Определяет Sequelize-модели для всех таблиц
3. Загружает `adminjs`, `@adminjs/express`, `@adminjs/sequelize` через `await import()`
4. Регистрирует адаптер, создаёт экземпляр AdminJS с ресурсами
5. Монтирует router через `app.use('/admin', authCheck, adminRouter)`
Авторизация в AdminJS **не использует** встроенный механизм логина — вместо этого проверяется основная сессия Express (`req.session.user.role === 'admin'`). Статические ресурсы AdminJS (`.js`, `.css`, `.woff`) пропускаются без проверки.
---
## 3. Инструменты
Платформа содержит **16 инструментов** в 4 категориях. 5 инструментов работают полностью на фронтенде без серверных вызовов.
---
### Категория: Изображения
#### Конвертер изображений (`/compress`)
**Что делает:** массовая конвертация и сжатие изображений с изменением формата и размера. Результат упаковывается в ZIP-архив.
**Бэкенд:** `routes/compress.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /compress` | GET | Страница инструмента |
| `POST /compress` | POST | Загрузка файлов, обработка, возврат ссылки на ZIP |
| `GET /download/:filename` | GET | Скачивание ZIP (монтируется в server.js) |
**Параметры POST-запроса (multipart/form-data):**
- `images[]` — файлы (до 50 штук)
- `format``original`, `webp`, `jpeg`, `png`, `avif`, `tiff`, `gif`
- `quality` — 1100 (по умолчанию из `COMPRESS_QUALITY` в .env, обычно 80)
- `resize` — максимальный размер по длинной стороне в пикселях (0 = без ресайза)
**Зависимости:** `sharp`, `archiver`, `multer`, `geoip-lite`
**Принцип работы:**
1. Multer сохраняет файлы во временный `uploads/`
2. Sharp обрабатывает каждый файл (конвертация + опциональный ресайз)
3. Буферы добавляются в ZIP через archiver
4. ZIP сохраняется в `downloads/`, клиенту возвращается URL
5. Загруженные файлы удаляются немедленно, ZIP — через 30 минут
6. Кириллические имена файлов транслитерируются
**Лимиты:**
- Размер файла: `MAX_FILE_SIZE_MB` (по умолчанию 20 MB)
- Количество файлов: `MAX_FILES` (по умолчанию 50)
- Rate limit: `RATE_LIMIT_MAX` запросов за `RATE_LIMIT_WINDOW_MS` (по умолчанию 30/мин)
**Поддерживаемые форматы входа:** JPEG, PNG, WebP, AVIF, TIFF, GIF, BMP, SVG
**Фронтенд:** `public/compress.html` — drag & drop загрузка, превью файлов, прогресс-бар, таблица статистики сжатия.
---
#### Placeholder (`/placeholder`)
**Что делает:** генерирует placeholder-изображения по URL-шаблону. Используется в разработке вместо реальных картинок. Работает **без авторизации**.
**Бэкенд:** `routes/placeholder.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /placeholder` | GET | Страница с документацией |
| `GET /placeholder/:size/:bg/:fg` | GET | Генерация изображения |
| `GET /placeholder/:size/:bg/:fg/:text` | GET | С кастомным текстом |
| `GET /placeholder-img/:size/:bg/:fg` | GET | Алиас (тот же роутер) |
**Параметры URL:**
- `size``300x200`, `1920x1080` и т.д. (макс. 4000x4000)
- `bg` — цвет фона в hex без `#` (например `cccccc`)
- `fg` — цвет текста
- `text` — текст поверх изображения (опционально)
- `?format=png|jpg|webp` — формат вывода (по умолчанию png)
- `?fontsize=N` — размер шрифта (по умолчанию автоматически)
**Пример:** `GET /placeholder/800x400/0054e6/ffffff/Preview`
**Зависимости:** `sharp` (SVG → растровое)
**Фронтенд:** `public/placeholder.html` — конструктор URL, предпросмотр, код для вставки.
---
#### SVG-редактор (`/svgeditor`)
**Что делает:** редактирование SVG-кода с предпросмотром, оптимизацией и генерацией простых иконок по ключевому слову.
**Бэкенд:** `routes/svgeditor.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /svgeditor` | GET | Страница редактора (full-screen) |
| `POST /api/svg-optimize` | POST | Оптимизация SVG (до 5 MB) |
| `POST /api/svg-ai` | POST | Генерация иконки по ключевому слову |
**Оптимизация** (`/api/svg-optimize`) выполняется на сервере regexp-преобразованиями:
- Удаление `<?xml>`, комментариев, `<metadata>`, `<title>`, `<desc>`
- Удаление пустых `<g>` и `data-*` атрибутов
- Округление чисел до 2 знаков, сжатие пробелов
**AI-генерация** (`/api/svg-ai`) — это lookup-таблица из ~20 предопределённых SVG-путей (home, user, search, heart, star, и т.д.) в стилях outline/filled/duotone. Не использует внешний AI.
**Параметры `/api/svg-ai`:** `{ keyword, style: "outline"|"filled"|"duotone", size, color }`
**Фронтенд:** `public/svgeditor.html` — полноэкранный редактор, split-панель код/превью.
---
#### Фоторедактор (`/editor`)
**Что делает:** браузерный редактор изображений на Canvas API. Работает **полностью на фронтенде** — серверных вызовов нет.
**Бэкенд:** `routes/pages.js` — только отдаёт `editor.html`
**Фронтенд:** `public/editor.html` + `public/editor.js` — полноэкранный редактор.
Возможности (реализованы на Canvas): обрезка, яркость/контраст/насыщенность, фильтры, вращение, отражение, сохранение в JPEG/PNG.
---
#### Видео конвертер (`/video`)
**Что делает:** конвертация видеофайлов между форматами, сжатие, извлечение аудио, создание GIF. Обработка асинхронная с polling-прогресса.
**Бэкенд:** `routes/video.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /video` | GET | Страница инструмента |
| `POST /video/upload` | POST | Загрузка файла, получение `jobId` и метаданных |
| `POST /video/convert` | POST (JSON) | Запуск конвертации, возвращает немедленно |
| `GET /video/progress/:jobId` | GET | Статус и прогресс задачи (0100%) |
| `GET /video/download/:filename` | GET | Скачивание результата |
**Режимы конвертации (`mode`):**
- `convert` — смена формата (mp4, webm, avi, mkv)
- `compress` — сжатие с выбором качества (high/medium/low)
- `audio` — извлечение аудио (mp3, aac)
- `gif` — конвертация в анимированный GIF (макс. ширина 480px, 12 fps)
**Зависимости:** FFmpeg, ffprobe (системные), `multer`
**Лимиты:**
- Обычный пользователь: 200 MB
- Администратор: 1 GB
- Таймаут FFmpeg: 10 минут
- Rate limit: 10 запросов/мин
**Хранение задач:** in-memory Map `jobs`, автоочистка через 30 минут. При рестарте сервера все задачи теряются.
**Прогресс:** FFmpeg пишет в stderr строки вида `time=00:01:23`, роут парсит их и обновляет `job.progress`.
**Фронтенд:** `public/video.html` — загрузка, выбор режима и параметров, polling прогресса, скачивание.
---
### Категория: Код
#### Code Formatter (`/formatter`)
**Что делает:** форматирование CSS и JavaScript с правильными отступами. Работает **полностью на фронтенде**.
**Бэкенд:** только `GET /formatter``formatter.html`
**Алгоритм форматирования** реализован в `formatter.html` через regexp:
- Строки и комментарии сохраняются в массив (не затрагиваются)
- Расставляются переносы строк вокруг `{` и `}`
- Применяется отступ 2 пробела с счётчиком вложенности
Нет зависимости от сторонних библиотек (prettier, babel и т.д.).
**Фронтенд:** `public/formatter.html` — две textarea (input/output), переключатель CSS/JS, кнопки копировать/очистить, счётчик символов.
---
#### HTML Sanitizer (`/sanitizer`)
**Что делает:** очистка HTML от потенциально опасных тегов и атрибутов. Работает **полностью на фронтенде** через браузерный DOM.
**Бэкенд:** только `GET /sanitizer``sanitizer.html`
**Фронтенд:** `public/sanitizer.html`
---
#### Format Converter (`/converter`)
**Что делает:** конвертация данных между форматами (JSON, YAML, CSV и т.д.). Работает **полностью на фронтенде**.
**Бэкенд:** только `GET /converter``converter.html`
**Фронтенд:** `public/converter.html`
---
### Категория: Веб
#### Парсер статей (`/parser`)
**Что делает:** извлекает основной текст статьи из URL с помощью алгоритма Readability (тот же, что использует Firefox Reader View). Возвращает заголовок, автора, текст, HTML, изображения, ссылки.
**Бэкенд:** `routes/parser.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /parser` | GET | Страница инструмента |
| `GET /parse?url=` | GET | Полный разбор статьи |
| `GET /metadata?url=` | GET | Только OG/мета-теги |
| `GET /text?url=` | GET | Только текст (text/plain) |
| `GET /preview?url=` | GET | Карточка предпросмотра |
**Зависимости:** `@mozilla/readability`, `jsdom`, `iconv-lite`, `geoip-lite`
**Особенности:**
- SSRF-защита через `lib/ssrf.js` на всех эндпоинтах
- In-memory кэш: 50 записей, TTL 10 минут
- Определение кодировки из `Content-Type` и `<meta charset>`
- Лимит страницы: 2 MB
- Таймаут запроса: 10 секунд
- Rate limit: 20 запросов/мин
**Ответ `/parse`:**
```json
{
"url": "...",
"title": "...",
"author": "...",
"site": "...",
"date_published": "...",
"excerpt": "...",
"content_html": "...",
"content_text": "...",
"lead_image": "...",
"images": [...],
"links": [{"text": "...", "href": "..."}],
"word_count": 1234,
"lang": "ru"
}
```
**Фронтенд:** `public/parser.html` — поле URL, вкладки с результатами (текст, HTML, метаданные, изображения).
---
#### HTTP-клиент (`/httpclient`)
**Что делает:** выполняет произвольные HTTP-запросы с сервера (полезно для тестирования API, недоступных из браузера из-за CORS). История сохраняется в сессии.
**Бэкенд:** `routes/httpclient.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /httpclient` | GET | Страница инструмента |
| `POST /api/proxy` | POST (JSON) | Выполнение HTTP-запроса |
| `GET /api/history` | GET | История запросов сессии (до 30) |
| `POST /api/history/add` | POST | Добавление записи в историю |
| `DELETE /api/history` | DELETE | Очистка истории |
**Параметры `/api/proxy`:**
```json
{
"url": "https://example.com/api",
"method": "POST",
"headers": {"Authorization": "Bearer ..."},
"body": "{\"key\": \"value\"}",
"timeout": 30000
}
```
**Ответ `/api/proxy`:**
```json
{
"status": 200,
"statusText": "OK",
"headers": {...},
"body": "...",
"time": 234,
"size": 1024,
"truncated": false,
"url": "..."
}
```
**Ограничения:**
- Тело ответа обрезается до 10 MB
- Таймаут: до 60 секунд (параметр `timeout`, но не более 60000 мс)
- SSRF-защита: приватные IP и localhost заблокированы
- Хедеры `host`, `connection`, `transfer-encoding` фильтруются из запроса
- Rate limit прокси: `PROXY_RATE_LIMIT_MAX` (по умолчанию 60/мин)
**Фронтенд:** `public/httpclient.html` — Postman-подобный интерфейс: метод, URL, заголовки, тело, подсветка ответа, история.
---
#### Redirect-анализатор (`/redirects`)
**Что делает:** пошагово обходит цепочку HTTP-редиректов, показывает каждый шаг (статус, Location, время), выявляет проблемы (длинная цепочка, смешанный HTTP/HTTPS, 302 вместо 301).
**Бэкенд:** `routes/redirects.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /redirects` | GET | Страница инструмента |
| `POST /api/redirect-analyze` | POST (JSON) | Анализ цепочки редиректов |
**Параметры:**
```json
{
"url": "https://example.com",
"userAgent": "desktop|mobile|googlebot",
"method": "GET|HEAD"
}
```
**Ответ:**
```json
{
"chain": [
{"url": "...", "status": 301, "statusText": "Moved Permanently", "location": "...", "time": 123, "headers": {...}},
{"url": "...", "status": 200, "statusText": "OK", "location": null, "time": 456, "headers": {...}}
],
"final_url": "...",
"total_time": 579,
"loop_detected": false,
"issues": ["long_chain", "302_not_301"]
}
```
**Ограничения:**
- Максимум 15 шагов в цепочке
- Таймаут на каждый шаг: 10 секунд
- SSRF-защита
- Rate limit: 20 запросов/мин
**Фронтенд:** `public/redirects.html` — визуальная цепочка шагов, выделение ошибок, подсказки.
---
### Категория: Утилиты
#### Генератор паролей (`/password`)
**Что делает:** генерация случайных паролей с настройкой длины и набора символов. Работает **полностью на фронтенде** через `crypto.getRandomValues()`.
**Бэкенд:** только `GET /password``password.html`
**Фронтенд:** `public/password.html` — слайдер длины, чекбоксы наборов символов, индикатор сложности, кнопка копирования.
---
#### Markdown Viewer (`/md`)
**Что делает:** рендеринг Markdown в HTML с предпросмотром. Работает **полностью на фронтенде**.
**Бэкенд:** только `GET /md``md.html`
**Фронтенд:** `public/md.html` — split-панель редактор/превью, библиотека marked.js.
---
#### PDF инструменты (`/pdf`)
**Что делает:** полный набор операций с PDF-файлами: объединение, разделение, поворот, удаление страниц, перестановка, водяной знак, нумерация, сжатие через Ghostscript, защита паролем, конвертация PDF → изображения и изображений → PDF.
**Бэкенд:** `routes/pdf.js`
| Эндпоинт | Метод | Описание |
|----------|-------|----------|
| `GET /pdf` | GET | Страница инструмента |
| `POST /pdf/upload` | POST | Загрузка PDF/изображений, возврат `fileId` |
| `GET /pdf/preview/:fileId` | GET | Превью первой страницы (PNG, Ghostscript) |
| `GET /pdf/preview/:fileId/:page` | GET | Превью конкретной страницы |
| `GET /pdf/thumbnails/:fileId` | GET | Список URL превью всех страниц |
| `GET /pdf/info/:fileId` | GET | Метаданные PDF (страницы, автор, заголовок) |
| `POST /pdf/merge` | POST (JSON) | Объединение нескольких PDF |
| `POST /pdf/split` | POST (JSON) | Извлечение страниц по диапазону |
| `POST /pdf/rotate` | POST (JSON) | Поворот страниц |
| `POST /pdf/delete` | POST (JSON) | Удаление страниц |
| `POST /pdf/reorder` | POST (JSON) | Перестановка страниц |
| `POST /pdf/watermark` | POST (JSON) | Добавление текстового водяного знака |
| `POST /pdf/pagenumbers` | POST (JSON) | Нумерация страниц |
| `POST /pdf/compress` | POST (JSON) | Сжатие через Ghostscript |
| `POST /pdf/protect` | POST (JSON) | Защита паролем (pdf-lib encrypt) |
| `POST /pdf/extract-text` | POST (JSON) | Извлечение текста |
| `POST /pdf/toImages` | POST (JSON) | PDF → изображения ZIP (Ghostscript) |
| `POST /pdf/fromImages` | POST | Изображения → PDF (pdf-lib) |
| `GET /pdf/download/:filename` | GET | Скачивание результата |
**Зависимости:** `pdf-lib` (ESM, через dynamic import), `pdf-parse`, Ghostscript (системный, команда `gs`)
**Хранение файлов:** in-memory Map `pdfFiles` с `fileId`, автоочистка через 30 минут.
**Превью** кэшируется на диске в `downloads/preview_<fileId>_p<N>.png`, удаляется через 30 минут.
**Лимиты:**
- Размер файла: 50 MB
- До 10 файлов за раз при загрузке
- До 50 изображений при конвертации из картинок
**Фронтенд:** `public/pdf.html` — drag & drop загрузка, визуальная сетка миниатюр страниц, панель операций.
---
## 4. Авторизация и роли
### Flow регистрации
1. `GET /auth/register` → форма `register.html`
2. `POST /auth/register` — валидация email (regexp), пароль ≥ 6 символов, проверка на дубликат
3. Хеширование пароля `bcrypt.hash(password, 10)`
4. Запись в `users`: email (lowercase), password_hash, display_name
5. Создание сессии `req.session.user = { id, email, name, role: 'user' }`
6. Редирект на `/dashboard`
`display_name` санируется: HTML-теги удаляются, длина ограничена 100 символами.
### Flow логина
1. `GET /auth/login` → форма `login.html`
2. `POST /auth/login` — поиск пользователя по email, проверка `is_blocked`, `bcrypt.compare`
3. `db.updateLastLogin(id)` — обновление поля `last_login`
4. **Session regeneration** (`req.session.regenerate`) для защиты от session fixation
5. Новая сессия с данными пользователя
6. Редирект на `returnTo` или `/dashboard`
### Структура сессии
```js
req.session.user = {
id: 42,
email: "user@example.com",
name: "Иван",
role: "user" // или "admin"
}
```
Дополнительные поля сессии:
- `req.session.returnTo` — URL для редиректа после логина
- `req.session.httpHistory` — история HTTP-клиента (до 30 записей)
### Роли
| Возможность | `user` | `admin` |
|-------------|--------|---------|
| Доступ к инструментам | да | да |
| Лимит загрузки видео | 200 MB | 1 GB |
| Ссылка «Админ» в сайдбаре | нет | да |
| Доступ к `/admin` | нет | да |
### Публичные маршруты (без авторизации)
- Лендинг `/`
- Страницы: `/auth/login`, `/auth/register`
- API лендинга: `/api/settings`, `/api/tools`, `/api/content/advantages`, `/api/content/dashboard`
- Статус: `/status`, `/status/api`
- Placeholder: `/placeholder`, `/placeholder-img/*`
- Статика: `shared.css`, `shared.js`, `landing.html`, всё из `/vendor/`
### Конфигурация сессии
```js
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 1 день
httpOnly: true, // недоступно JS
secure: NODE_ENV === 'production', // только HTTPS
sameSite: 'lax', // защита от CSRF
}
})
```
Сессии хранятся in-memory (MemoryStore Express). При рестарте PM2 все сессии теряются — пользователям нужно войти заново.
### Rate limiting для auth
Rate limit работает per-user: `keyGenerator` использует `req.session.user.id` для авторизованных пользователей, `req.ip` для анонимных. Это предотвращает ситуацию, когда один бот исчерпывает лимит для всех.
15 минут, 10 попыток (применяется к `POST /auth/login` и `POST /auth/register`).
---
## 5. AdminJS панель
**URL:** `https://wadevelop.ru/admin`
### Доступ
Доступ контролируется двойной проверкой:
1. Основная сессия Express (`req.session.user` должен существовать)
2. Роль администратора (`req.session.user.role === 'admin'`)
Если пользователь не авторизован или не является admin — редирект на `/auth/login` с `returnTo=/admin`. Встроенный логин AdminJS не используется.
### Управляемые сущности
| Сущность | Навигация | Описание |
|----------|-----------|----------|
| Users | Пользователи | Управление аккаунтами, блокировка, смена роли |
| Categories | Контент | Категории инструментов с иконками и цветами |
| Tools | Контент | Список инструментов, включение/отключение |
| ContentBlocks | Контент | Текстовые блоки лендинга (преимущества, контент дашборда) |
| Settings | Настройки | Ключ-значение настройки сайта |
### Смена пароля пользователя
В форме редактирования User есть два виртуальных поля: `password` и `password_confirm`. Они не хранятся в БД — вместо этого action-hook `before` обрабатывает их:
1. Если `password` пустой — пропуск (пароль не меняется)
2. Если `password !== password_confirm` — ошибка валидации
3. Если длина < 6 символов ошибка
4. Иначе: `bcrypt.hash(password, 10)` запись в `password_hash`
Поле `password_hash` скрыто в интерфейсе (`isVisible: false`).
### Особенности AdminJS
- Загрузка занимает ~6 секунд при старте сервера (ESM import + инициализация Sequelize)
- В первые секунды после рестарта PM2 `/admin` может возвращать 502
- Sequelize использует отдельное соединение, независимое от пула `lib/db.js`
---
## 6. База данных
**СУБД:** MariaDB 10.11
**База:** `wa_tools`
**Пользователь:** `wa_tools`
**Кодировка:** utf8mb4
### Таблица `users`
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | INT AUTO_INCREMENT PK | Идентификатор |
| `email` | VARCHAR(255) UNIQUE NOT NULL | Email (хранится в lowercase) |
| `password_hash` | VARCHAR(255) NOT NULL | bcrypt-хеш пароля |
| `display_name` | VARCHAR(100) | Отображаемое имя |
| `role` | ENUM('user', 'admin') | Роль, по умолчанию 'user' |
| `is_blocked` | BOOLEAN | Заблокирован ли аккаунт |
| `created_at` | DATETIME | Дата регистрации |
| `last_login` | DATETIME | Дата последнего входа |
### Таблица `settings`
Ключ-значение для настроек сайта. Отдаётся через `/api/settings` на лендинг.
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | INT AUTO_INCREMENT PK | Идентификатор |
| `setting_key` | VARCHAR(100) UNIQUE NOT NULL | Ключ настройки |
| `setting_value` | TEXT | Значение |
| `description` | VARCHAR(255) | Описание (для AdminJS) |
| `updated_at` | DATETIME | Дата последнего изменения |
### Таблица `categories`
Категории инструментов для лендинга и дашборда.
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | INT AUTO_INCREMENT PK | Идентификатор |
| `slug` | VARCHAR(50) UNIQUE NOT NULL | URL-идентификатор (images, code, web, utils) |
| `title` | VARCHAR(100) NOT NULL | Название |
| `description` | VARCHAR(255) | Краткое описание |
| `icon_svg` | TEXT | SVG-путь иконки |
| `color` | VARCHAR(20) | Цвет акцента (по умолчанию '#0054e6') |
| `sort_order` | INT | Порядок сортировки |
### Таблица `tools`
Список инструментов платформы.
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | INT AUTO_INCREMENT PK | Идентификатор |
| `slug` | VARCHAR(50) UNIQUE NOT NULL | URL-идентификатор |
| `title` | VARCHAR(100) NOT NULL | Название |
| `description` | VARCHAR(255) | Описание |
| `path` | VARCHAR(100) NOT NULL | URL-путь (например `/compress`) |
| `icon_svg` | TEXT | SVG-путь иконки |
| `category_id` | INT FK categories.id | Категория |
| `sort_order` | INT | Порядок сортировки |
| `is_enabled` | BOOLEAN | Отображается ли инструмент |
### Таблица `content_blocks`
Редактируемый контент для лендинга.
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | INT AUTO_INCREMENT PK | Идентификатор |
| `block_key` | VARCHAR(100) UNIQUE NOT NULL | Ключ блока |
| `title` | VARCHAR(255) | Заголовок блока |
| `body` | TEXT | Содержимое (HTML, richtext в AdminJS) |
| `section` | VARCHAR(50) | Секция: `advantages`, `dashboard` |
| `sort_order` | INT | Порядок |
| `is_visible` | BOOLEAN | Отображается ли блок |
| `updated_at` | DATETIME | Дата последнего изменения |
### Соединение с БД
`lib/db.js` использует `mysql2/promise` с connection pool (лимит 5 соединений). Предоставляет 4 хелпера: `findUserByEmail`, `findUserById`, `createUser`, `updateLastLogin`, и экспортирует `pool` для прямых запросов из роутов.
---
## 7. API Reference
### Публичные API (без авторизации)
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/api/settings` | Все настройки как `{key: value}` |
| GET | `/api/tools` | Категории с вложенными инструментами |
| GET | `/api/content/advantages` | Блоки секции advantages |
| GET | `/api/content/dashboard` | Блоки секции dashboard |
### Auth API
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/auth/login` | Страница логина |
| GET | `/auth/register` | Страница регистрации |
| POST | `/auth/login` | Вход: `{email, password}` `{ok, redirect}` |
| POST | `/auth/register` | Регистрация: `{email, password, password2, name}` `{ok, redirect}` |
| GET | `/auth/logout` | Выход, разрушение сессии |
| GET | `/auth/me` | Данные текущего пользователя `{id, email, name, role}` |
### Tool APIs (защищены)
#### Изображения
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/compress` | Страница инструмента |
| POST | `/compress` | `multipart: images[], format, quality, resize` `{downloadUrl, stats}` |
| GET | `/download/:filename` | Скачивание ZIP |
| GET | `/placeholder/:size/:bg/:fg` | Генерация placeholder (публично) |
| GET | `/placeholder/:size/:bg/:fg/:text` | С текстом (публично) |
| POST | `/api/svg-optimize` | `{svg}` `{svg, saved}` |
| POST | `/api/svg-ai` | `{keyword, style, size, color}` `{svg, keyword, available}` |
#### Видео
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/video` | Страница |
| POST | `/video/upload` | `multipart: video` `{jobId, info, originalName, size}` |
| POST | `/video/convert` | `{jobId, mode, format, quality, startTime, endTime}` `{status}` |
| GET | `/video/progress/:jobId` | `{status, progress, downloadUrl?, outputSize?, savings?, error?}` |
| GET | `/video/download/:filename` | Скачивание результата |
#### PDF
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/pdf` | Страница |
| POST | `/pdf/upload` | `multipart: files[]` `{files: [{id, name, size, pages}]}` |
| GET | `/pdf/preview/:fileId[/:page]` | PNG превью страницы |
| GET | `/pdf/thumbnails/:fileId` | `{pages, thumbnails: [...urls]}` |
| GET | `/pdf/info/:fileId` | `{pages, title, author, creator, size}` |
| POST | `/pdf/merge` | `{fileIds: [...]}` `{downloadUrl, size}` |
| POST | `/pdf/split` | `{fileId, ranges: "1-3,5"}` `{downloadUrl, size, pages}` |
| POST | `/pdf/rotate` | `{fileId, pages: "all"|"1,3", angle: 90}` `{downloadUrl, size}` |
| POST | `/pdf/delete` | `{fileId, pages: "2,4"}` `{downloadUrl, size, pages}` |
| POST | `/pdf/reorder` | `{fileId, order: [3,1,2]}` `{downloadUrl, size}` |
| POST | `/pdf/watermark` | `{fileId, text, fontSize, opacity, color}` `{downloadUrl, size}` |
| POST | `/pdf/pagenumbers` | `{fileId, position, startFrom}` `{downloadUrl, size}` |
| POST | `/pdf/compress` | `{fileId, quality: "screen"|"ebook"|"printer"}` `{downloadUrl, size, savings}` |
| POST | `/pdf/protect` | `{fileId, password}` `{downloadUrl, size}` |
| POST | `/pdf/extract-text` | `{fileId}` `{text, pages, metadata}` |
| POST | `/pdf/toImages` | `{fileId, format: "png"|"jpg", dpi: 150}` `{downloadUrl, size}` |
| POST | `/pdf/fromImages` | `multipart: images[]` `{downloadUrl, size, pages}` |
| GET | `/pdf/download/:filename` | Скачивание результата |
#### Веб-инструменты
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/parse?url=` | Полный разбор статьи |
| GET | `/metadata?url=` | Только метаданные |
| GET | `/text?url=` | Только текст |
| GET | `/preview?url=` | Карточка предпросмотра |
| POST | `/api/proxy` | HTTP-прокси запрос |
| GET | `/api/history` | История HTTP-клиента |
| POST | `/api/history/add` | Добавить в историю |
| DELETE | `/api/history` | Очистить историю |
| POST | `/api/redirect-analyze` | Анализ цепочки редиректов |
### System API
| Метод | Путь | Auth | Описание |
|-------|------|------|----------|
| GET | `/health` | нет | `{status, uptime, memory, node, pid}` |
| GET | `/status` | нет | Страница статуса |
| GET | `/status/api` | нет | `{overall, uptime, services, timestamp}` |
---
## 8. Фронтенд архитектура
### shared.css
Единый файл стилей для всех страниц. Структура:
**CSS-переменные** (`:root` и `.dark`):
- `--bg`, `--bg-grad1/2` фон страницы с радиальными градиентами
- `--surface-800/700/600` уровни поверхностей (фон карточек, бордеры)
- `--text-primary/secondary/muted` уровни текста
- `--accent`, `--accent-dim`, `--accent-bright`, `--accent-bg` акцентный синий (#0054e6)
**Компоненты:**
- `.sidebar` фиксированная боковая панель 56px (на мобильных: нижняя панель 52px)
- `.sidebar-link` кнопка инструмента 36x36px с активным состоянием и индикатором
- `.sidebar-avatar` аватар пользователя (первая буква имени)
- `.theme-toggle` переключатель темы
- `.top-header` верхняя шапка для публичных страниц
- `.tool-container` контейнер содержимого с padding
- `.wa-input`, `.wa-btn`, `.wa-card` базовые элементы форм
- `.wa-modal-overlay`, `.wa-modal` модальное окно с анимацией
### shared.js
Загружается на всех страницах с sidebar. Содержит:
**`WA_CATEGORIES`** массив категорий: `id`, `title`, `description`, `icon` (SVG-path), `tools` (массив slug-ов).
**`WA_TOOLS`** массив всех инструментов: `id`, `path`, `title`, `category`, `icon`, `adminOnly`. Определяет полный список сайдбара. Добавить новый инструмент = добавить объект сюда.
**`initSidebar(activeId)`** вставляет `<nav class="sidebar">` в `document.body.prepend()`. Автоматически определяет активный инструмент по `window.WA_TOOL_ID` (устанавливается в каждом HTML) или по URL. Группирует ссылки по категориям с разделителями.
**`_loadUser()`** вызывает `/auth/me`, заполняет аватар инициалами имени. Для admin-пользователей делает видимыми элементы `.sidebar-admin-link` (ссылку «Админ»).
**`_showLogoutModal(userName)`** модальное окно подтверждения выхода.
**`initTheme()`** читает `localStorage.theme`, устанавливает класс `.dark` на `<html>`. Переключение через клик на `#themeToggle`. Вызывается немедленно (до DOMContentLoaded) для предотвращения мигания.
**Tailwind config** тёмный режим через `class`, кастомные цвета `surface.*`, `accent.*`, шрифты Manrope и JetBrains Mono.
### Типы страниц
| Тип | Примеры | Layout |
|-----|---------|--------|
| Tool pages | `/compress`, `/pdf`, `/video` | Sidebar слева, `<div class="main-content">` |
| Public pages | `/`, `/auth/login`, `/status` | `top-header` сверху, без sidebar |
| Full-screen | `/editor`, `/svgeditor` | Без sidebar и top-header, 100vw/100vh |
### Страница инструмента (шаблон)
```html
<script>window.WA_TOOL_ID = 'compress';</script> <!-- устанавливает активный элемент sidebar -->
<script src="/shared.js?v=3"></script>
<link href="/shared.css" rel="stylesheet">
<body>
<!-- sidebar вставляется shared.js автоматически -->
<div class="main-content">
<div class="tool-container">
<!-- содержимое инструмента -->
</div>
</div>
</body>
```
### Лендинг (`/`)
`public/landing.html` публичная страница, загружает данные динамически:
- `/api/settings` настройки (заголовок, описание сайта)
- `/api/tools` список инструментов для секции категорий
- `/api/content/advantages` блоки преимуществ
Структура: hero с CTA секция категорий секция преимуществ footer.
### Дашборд (`/dashboard`)
`public/dashboard.html` главная страница для авторизованных пользователей. Загружает `/auth/me` для персонализации приветствия, `/api/tools` для отображения категорий с иконками.
### Яндекс Метрика
Счётчик `108185982` подключён на всех страницах инструментов (в конце `<body>`).
---
## 9. Безопасность
### HTTP-заголовки (helmet)
```js
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://mc.yandex.ru"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://mc.yandex.ru"],
connectSrc: ["'self'", "wss:", "ws:"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
crossOriginEmbedderPolicy: false,
})
```
CSP включён. `unsafe-inline` необходим для inline `<script>` и `<style>` блоков UI. Остальные политики helmet включены: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.
### Защита от SSRF (`lib/ssrf.js`)
Применяется в HTTP-клиенте, парсере и redirect-анализаторе. Два уровня проверки:
1. **Синтаксическая** только `http:` и `https:`, блокировка `localhost`, `*.local`, `*.internal`
2. **Диапазоны IP** (regexp):
- `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` приватные RFC 1918
- `127.0.0.0/8` loopback
- `169.254.0.0/16` link-local
- `::1`, `fc*/fd*`, `fe80*` IPv6 приватные
3. **DNS-резолвинг** hostname разрешается через `dns.resolve4`, все IP проверяются на попадание в приватные диапазоны
### Загрузка файлов
- MIME-тип валидируется через whitelist (multer `fileFilter`)
- Проверяется также расширение файла (regexp) как запасной вариант
- Максимальный размер задаётся через переменные окружения
- Временные файлы удаляются немедленно после обработки
### Path traversal
Имена файлов при скачивании обрабатываются через `path.basename(req.params.filename)` попытки выйти из директории (`../`) отсекаются.
### Пароли
- bcrypt с `saltRounds: 10`
- Минимальная длина: 6 символов
- Оригинальный пароль никогда не логируется
### Сессия
- Хранение: MariaDB через `express-mysql-session` (таблица `sessions`)
- Сессии переживают рестарт PM2 пользователи остаются залогиненными
- `httpOnly: true` недоступно JavaScript
- `secure: true` в production только по HTTPS
- `sameSite: 'lax'` защита от CSRF
- Session regeneration при логине защита от session fixation attack
- Logout: `req.session.destroy()`
- Автоочистка: `checkExpirationInterval: 900000` (15 мин), `expiration: 86400000` (24ч)
### Санитизация ввода
- `display_name` HTML-теги удаляются regexp'ом, длина обрезается до 100
- Email нормализуется в lowercase, проверяется regexp
### AdminJS
Двойная проверка: наличие сессии + role === 'admin'. Статические ресурсы AdminJS (`.js`, `.css`, `.woff` и т.д.) пропускаются без auth-проверки по расширению.
---
## 10. Деплой и инфраструктура
### PM2
Конфигурация в `ecosystem.config.js`:
```js
{
name: 'images',
script: 'server.js',
cwd: '/mnt/webdata/www/images.wadevelop.ru',
instances: 1,
exec_mode: 'fork',
max_memory_restart: '256M',
restart_delay: 3000,
max_restarts: 10,
min_uptime: 5000,
}
```
Логи:
- stdout: `~/.pm2/logs/images-out.log`
- stderr: `~/.pm2/logs/images-error.log`
- Формат времени: `YYYY-MM-DD HH:mm:ss`
Команды управления:
```bash
pm2 start ecosystem.config.js # первый запуск
pm2 restart images # перезапуск
pm2 reload images # graceful reload (0-downtime)
pm2 logs images # просмотр логов
pm2 monit # мониторинг в реальном времени
```
### Nginx
**wadevelop.ru** основной сайт:
```nginx
server {
server_name wadevelop.ru www.wadevelop.ru;
listen 443 ssl;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 200M; # для загрузки видео
}
}
# HTTP → HTTPS редирект управляется certbot
```
**images.wadevelop.ru** 301 редирект на wadevelop.ru (историческое доменное имя).
Логи nginx:
- `/var/log/nginx/wadevelop.access.log`
- `/var/log/nginx/wadevelop.error.log`
### SSL
Let's Encrypt через certbot. Сертификат:
- `/etc/letsencrypt/live/wadevelop.ru/fullchain.pem`
- `/etc/letsencrypt/live/wadevelop.ru/privkey.pem`
Автообновление через certbot systemd timer.
### Переменные окружения (`.env`)
| Переменная | Описание | По умолчанию |
|-----------|----------|--------------|
| `PORT` | Порт Express | 3000 |
| `NODE_ENV` | Окружение (`production`/`development`) | |
| `SESSION_SECRET` | Секрет сессий (обязательно сменить) | 'change-me-in-env' |
| `DB_HOST` | Хост MariaDB | localhost |
| `DB_USER` | Пользователь БД | wa_tools |
| `DB_PASSWORD` | Пароль БД | |
| `DB_NAME` | Имя базы данных | wa_tools |
| `COMPRESS_QUALITY` | Качество сжатия изображений по умолчанию | 60 |
| `MAX_FILE_SIZE_MB` | Максимальный размер файла для конвертера изображений (MB) | 20 |
| `MAX_FILES` | Максимальное количество файлов за раз | 50 |
| `RATE_LIMIT_WINDOW_MS` | Окно rate limit (мс) | 60000 |
| `RATE_LIMIT_MAX` | Максимум запросов за окно (compress) | 30 |
| `PROXY_RATE_LIMIT_MAX` | Максимум запросов за окно (http proxy) | 60 |
| `LOG_LEVEL` | Уровень логирования (`error`/`warn`/`info`/`debug`) | info |
---
## 11. Мониторинг
### Страница статуса `/status`
Публичная страница, доступная без авторизации. API: `GET /status/api`.
Проверяемые сервисы:
- **API** всегда `operational` (если сервер отвечает)
- **Database** `pool.execute('SELECT 1')`, `operational` или `outage`
- **Image Processing** всегда `operational`
- **Video Processing** всегда `operational`
- **Authentication** всегда `operational`
Ответ:
```json
{
"overall": "operational",
"uptime": 86400,
"services": [
{"name": "API", "status": "operational"},
{"name": "Database", "status": "operational"}
],
"timestamp": "2026-03-22T10:00:00.000Z"
}
```
### Health check `/health`
```json
{
"status": "ok",
"uptime": 86400,
"memory": {
"rss": "302MB",
"heap": "86/91MB",
"system": "37%"
},
"cpu": {
"load1m": "0.00",
"load5m": "0.00",
"load15m": "0.05",
"cores": 4
},
"queue": {
"stats": { "done": 5, "processing": 1 },
"active": 1,
"pending": 0
},
"ws": {
"connections": 2
},
"node": "v20.19.5",
"pid": 12345
}
```
Расширенный health: системная память, CPU load, глубина очереди задач, активные WebSocket подключения.
### Логирование
**pino** (`lib/logger.js`):
- Формат: структурированный JSON (`{"level":30,"time":...,"msg":"..."}`)
- Уровни: `error`, `warn`, `info`, `debug` (контролируется через `LOG_LEVEL`)
- API совместим: `log.info(msg, meta)`, `log.error(msg, meta)`, `log.logToFile(text)`
- PM2 перенаправляет stdout/stderr в файлы логов
**logToFile** append-запись в файл для операций сжатия/парсинга:
- Формат: `[ISO timestamp] [RU] 1.2.3.4 compress: file.jpg 800x600 format=webp q=80 (234KB)`
- Включает страну из GeoIP, IP, имя файла, параметры, размер
---
## 12. Известные ограничения
### AdminJS: медленная загрузка
ESM-импорт AdminJS при старте занимает ~6 секунд. В течение этого времени `/admin` возвращает 404. После рестарта PM2 нужно подождать перед открытием панели.
### Память (swap)
Swap загружен на ~100% из-за LLM-кластера (llama.cpp + open-webui ~3.8 GB). PM2 перезапустит процесс при превышении 256 MB RSS.
### PM2 cluster mode
Несовместим с текущей архитектурой AdminJS (ESM dynamic import + in-memory state). Работает только в fork mode (`instances: 1`).
### Производительность конвертации
Sharp нативный, быстрый. FFmpeg и Ghostscript CPU-интенсивные. На Raspberry Pi 5 длинное видео может конвертироваться несколько минут.
### PDF-файлы
Загруженные PDF хранятся в in-memory Map с таймаутом 30 мин. При рестарте теряются (в отличие от видео-задач, которые уже в SQLite-очереди).
---
## 13. Будущие доработки
### OAuth авторизация
Добавить вход через Google и Яндекс. Пакет `openid-client` уже установлен в зависимостях, `passport` тоже присутствует реализация не завершена.
### Улучшения инструментов
| Инструмент | Улучшение |
|-----------|-----------|
| Конвертер изображений | Quality slider в UI (сейчас фиксируется в .env) |
| Генератор паролей | Режим passphrase (несколько слов через дефис) |
| HTTP-клиент | Поддержка переменных окружения (как в Postman) |
### Usage analytics
Логирование использования инструментов в отдельную таблицу БД для аналитики популярности.
### PDF-файлы в очередь
Перенести `pdfFiles Map` в SQLite-очередь (аналогично видео-задачам) для персистентности.
### Монетизация
Тарифы, API-ключи, billing usage, multi-tenant лимиты.