# 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` — 1–100 (по умолчанию из `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 | Статус и прогресс задачи (0–100%) |
| `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 лимиты.
