wa-dev-tools/README.md
2026-03-22 01:19:23 +03:00

653 lines
30 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
Веб-платформа с набором инструментов для разработчиков. Включает 15 утилит для работы с изображениями, видео, PDF, кодом и веб-ресурсами — всё в одном защищённом интерфейсе с авторизацией.
**Продакшн:** [wadevelop.ru](https://wadevelop.ru) / [images.wadevelop.ru](https://images.wadevelop.ru)
---
## Содержание
1. [Быстрый старт](#быстрый-старт)
2. [Архитектура](#архитектура)
3. [Инструменты](#инструменты)
4. [Авторизация](#авторизация)
5. [Админ-панель](#админ-панель)
6. [API Reference](#api-reference)
7. [Безопасность](#безопасность)
8. [Конфигурация](#конфигурация)
9. [Деплой](#деплой)
10. [Мониторинг](#мониторинг)
11. [Технологии](#технологии)
12. [База данных](#база-данных)
---
## Быстрый старт
### Требования
- Node.js 18+
- MariaDB 10.6+
- FFmpeg (для видеоконвертера)
- Ghostscript (для сжатия и конвертации PDF)
### Установка
```bash
# Клонировать репозиторий
git clone <repo-url>
cd wa-dev-tools
# Установить зависимости
npm install
# Создать файл конфигурации
cp .env.example .env
# Заполнить переменные (см. раздел Конфигурация)
# Создать базу данных
mysql -u root -p < schema.sql
# Запустить в режиме разработки
node server.js
# Или через PM2 в продакшне
pm2 start ecosystem.config.js
```
Сервер стартует на `http://localhost:3000` (порт задаётся через `PORT` в `.env`).
---
## Архитектура
Приложение построено на **Express 5** с модульной маршрутизацией. Каждый инструмент изолирован в отдельном файле роутера. Общая библиотека (`lib/`) предоставляет auth middleware, подключение к БД, логгер и SSRF-защиту.
```
wa-dev-tools/
├── server.js # Точка входа: Express, сессии, middleware, AdminJS
├── ecosystem.config.js # PM2-конфигурация
├── lib/
│ ├── auth.js # Middleware авторизации (список публичных путей)
│ ├── admin.js # AdminJS: Sequelize-модели, маунт /admin
│ ├── db.js # Пул соединений mysql2, CRUD-функции для users
│ ├── logger.js # Структурированный логгер (stdout + файл)
│ └── ssrf.js # Валидация URL, блокировка приватных IP и диапазонов
├── routes/
│ ├── api.js # Публичный REST API (settings, tools, content)
│ ├── auth.js # Регистрация, вход, выход, /auth/me
│ ├── compress.js # Сжатие изображений (Sharp + Archiver)
│ ├── placeholder.js # Генератор placeholder-изображений
│ ├── svgeditor.js # SVG-оптимизация и AI-генерация иконок
│ ├── video.js # Конвертер видео (FFmpeg, асинхронные задачи)
│ ├── pdf.js # PDF-инструменты (pdf-lib, Ghostscript)
│ ├── parser.js # Парсер статей (Readability, JSDOM, кеш)
│ ├── httpclient.js # HTTP-клиент (прокси-запросы, история сессии)
│ ├── redirects.js # Анализатор цепочек редиректов
│ ├── pages.js # HTML-страницы инструментов (formatter, md и др.)
│ ├── status.js # Страница статуса сервисов
│ └── logs.js # Просмотр логов (защищённый)
└── public/
├── shared.js # Сайдбар, темы, список инструментов (клиент)
├── shared.css # Базовые стили
├── landing.html # Публичная лендинг-страница
├── login.html # Страница входа
├── register.html # Страница регистрации
└── *.html # Страницы инструментов
```
### Порядок middleware в server.js
```
helmet (security headers)
express-session
express.static (public/) — всегда публично
/auth/* — публично (вход/регистрация)
/api/settings, /api/tools, /api/content/* — публично (лендинг)
/status — публично
authMiddleware — всё ниже требует сессии
/api/*, /compress/*, /video/*, /pdf/*, /parser, ...
```
---
## Инструменты
### Изображения
| Инструмент | Путь | Описание |
|---|---|---|
| Конвертер изображений | `/compress` | Пакетное сжатие и конвертация до 50 файлов. Форматы: JPEG, PNG, WebP, AVIF, TIFF, GIF. Настройка качества и максимальной стороны. Результат — ZIP-архив. |
| Placeholder | `/placeholder` | Генератор заглушек для макетов. API-эндпоинт `/placeholder/:size/:bg/:fg/:text` возвращает PNG/JPEG/WebP без авторизации. |
| SVG-редактор | `/svgeditor` | Оптимизация SVG (удаление комментариев, metadata, пустых групп, обрезка чисел). Генерация простых иконок по ключевому слову (20 встроенных шаблонов). |
| Фоторедактор | `/editor` | Клиентский редактор изображений. |
| Видеоконвертер | `/video` | Конвертация, сжатие, извлечение аудио и создание GIF через FFmpeg. Асинхронная обработка с поллингом прогресса. |
### Код
| Инструмент | Путь | Описание |
|---|---|---|
| Code Formatter | `/formatter` | Форматирование кода на стороне клиента. |
| HTML Sanitizer | `/sanitizer` | Очистка HTML от потенциально опасных тегов и атрибутов. |
| Конвертер форматов | `/converter` | Конвертация между форматами данных (JSON, YAML, TOML и др.). |
### Веб
| Инструмент | Путь | Описание |
|---|---|---|
| Парсер статей | `/parser` | Извлечение текста и метаданных из URL через Mozilla Readability. Поддержка кодировок (iconv-lite), кеш 50 записей / 10 мин, автоматическая смена кодировки. |
| HTTP-клиент | `/httpclient` | Прокси-запросы к внешним URL: GET/POST/PUT/DELETE, произвольные заголовки, тело запроса, таймаут до 60 сек. История запросов в сессии (до 30 записей). |
| Redirect-анализатор | `/redirects` | Трассировка цепочки редиректов до 15 шагов. Три User-Agent профиля: desktop, mobile, Googlebot. Детектирует длинные цепочки, смешанный протокол HTTP→HTTPS, временные 302-редиректы. |
### Утилиты
| Инструмент | Путь | Описание |
|---|---|---|
| Генератор паролей | `/password` | Клиентская генерация паролей с настройкой сложности. |
| Markdown Viewer | `/md` | Рендер Markdown в HTML. |
| PDF-инструменты | `/pdf` | Merge, Split, Rotate, Delete pages, Reorder, Watermark, Page numbers, Compress (Ghostscript), Protect (пароль), Extract text, PDF→Images, Images→PDF. |
---
## Авторизация
Используется локальная авторизация на основе email + пароль.
**Технический стек:** bcrypt (10 rounds), express-session, MariaDB.
### Процесс входа
1. Пользователь отправляет `POST /auth/login` с `email` и `password`.
2. Пароль сравнивается с `password_hash` в таблице `users` через `bcrypt.compare`.
3. При успехе сессия перегенерируется (`req.session.regenerate`) для защиты от session fixation.
4. В сессии сохраняется объект `{ id, email, name, role }`.
### Процесс регистрации
```
POST /auth/register
Body: { email, password, password2, name }
```
Валидация: формат email, минимум 6 символов пароля, совпадение паролей, уникальность email. Имя пользователя очищается от HTML-тегов.
### Сессия
```javascript
// Конфигурация сессии (server.js)
{
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000, // 24 часа
httpOnly: true,
secure: true, // только в production
sameSite: 'lax',
}
}
```
### Роли
| Роль | Доступ |
|---|---|
| `user` | Все инструменты платформы |
| `admin` | Все инструменты + ссылка на `/admin` в сайдбаре + доступ к AdminJS |
Заблокированные аккаунты (`is_blocked = true`) получают `403` при попытке входа.
### Публичные пути (без авторизации)
- `/` — лендинг
- `/auth/*` — вход, регистрация, выход
- `/status` — страница статуса
- `/placeholder`, `/placeholder-img/*` — API заглушек
- `/api/settings`, `/api/tools`, `/api/content/advantages`, `/api/content/dashboard` — данные для лендинга
- `/health` — health check
---
## Админ-панель
Доступна по адресу `/admin`. Требует роль `admin` в основной сессии сайта — отдельная авторизация AdminJS не используется.
Построена на **AdminJS 7** + **@adminjs/sequelize**. Модели подключены через Sequelize (параллельно с основным пулом mysql2).
### Управляемые сущности
| Раздел | Модель | Таблица | Что можно делать |
|---|---|---|---|
| Пользователи | `User` | `users` | Просмотр, создание, редактирование, блокировка, смена роли и пароля |
| Контент | `Category` | `categories` | CRUD категорий инструментов |
| Контент | `Tool` | `tools` | CRUD инструментов: включение/отключение, порядок сортировки, иконки |
| Контент | `ContentBlock` | `content_blocks` | Редактирование текстовых блоков лендинга (richtext) |
| Настройки | `Setting` | `settings` | Редактирование значений настроек приложения |
Смена пароля через AdminJS: пароль хешируется bcrypt'ом в before-хуке действия `edit` / `new`. Поле `password_hash` скрыто из интерфейса.
---
## API Reference
### Публичные эндпоинты
| Метод | Путь | Описание |
|---|---|---|
| `GET` | `/api/settings` | Все настройки как объект `{ key: value }` |
| `GET` | `/api/tools` | Категории с вложенными активными инструментами |
| `GET` | `/api/content/advantages` | Блоки контента секции `advantages` |
| `GET` | `/api/content/dashboard` | Блоки контента секции `dashboard` |
| `GET` | `/placeholder/:size/:bg/:fg` | Изображение-заглушка |
| `GET` | `/placeholder/:size/:bg/:fg/:text` | Изображение-заглушка с текстом |
| `GET` | `/status/api` | Статус сервисов |
| `GET` | `/health` | Uptime, память, версия Node |
**Пример запроса к placeholder API:**
```
GET /placeholder/800x600/0054e6/ffffff/Hello
```
Параметры: `size` = `WxH`, `bg` и `fg` = hex-цвет без `#`, `text` = текст на изображении. Формат: `?format=png|jpg|webp`.
### Защищённые эндпоинты (требуют сессию)
**Авторизация:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/auth/login` | Вход. Body: `{ email, password }` |
| `POST` | `/auth/register` | Регистрация. Body: `{ email, password, password2, name }` |
| `GET` | `/auth/logout` | Выход, уничтожение сессии |
| `GET` | `/auth/me` | Текущий пользователь `{ id, email, name, role }` |
**Изображения:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/compress` | Загрузка файлов (`multipart/form-data`, поле `images[]`). Параметры: `format`, `quality`, `resize`. Ответ: `{ downloadUrl, stats[] }` |
| `GET` | `/download/:filename` | Скачать ZIP с результатом |
**Видео:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/video/upload` | Загрузка видео (до 200MB). Ответ: `{ jobId, info }` |
| `POST` | `/video/convert` | Запуск обработки. Body: `{ jobId, mode, format, quality, startTime, endTime }` |
| `GET` | `/video/progress/:jobId` | Прогресс задачи. Ответ: `{ status, progress, downloadUrl?, savings? }` |
| `GET` | `/video/download/:filename` | Скачать результат |
Режимы конвертации (`mode`): `convert`, `compress`, `audio`, `gif`.
**PDF:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/pdf/upload` | Загрузка PDF/изображений (до 50MB, до 10 файлов). Ответ: `{ files: [{ id, name, size, pages }] }` |
| `GET` | `/pdf/info/:fileId` | Метаданные PDF (страницы, автор, создатель) |
| `POST` | `/pdf/merge` | Объединение. Body: `{ fileIds: [] }` |
| `POST` | `/pdf/split` | Разбивка страниц. Body: `{ fileId, ranges: "1-3,5" }` |
| `POST` | `/pdf/rotate` | Поворот. Body: `{ fileId, pages: "all" или "1,3", angle: 90 }` |
| `POST` | `/pdf/delete` | Удаление страниц. Body: `{ fileId, pages: "2,4" }` |
| `POST` | `/pdf/reorder` | Переупорядочивание. Body: `{ fileId, order: [3,1,2] }` |
| `POST` | `/pdf/watermark` | Водяной знак. Body: `{ fileId, text, fontSize, opacity, color }` |
| `POST` | `/pdf/pagenumbers` | Нумерация страниц. Body: `{ fileId, position, startFrom }` |
| `POST` | `/pdf/compress` | Сжатие через Ghostscript. Body: `{ fileId, quality: "screen"|"ebook"|"printer" }` |
| `POST` | `/pdf/protect` | Защита паролем. Body: `{ fileId, password }` |
| `POST` | `/pdf/extract-text` | Извлечение текста. Body: `{ fileId }` |
| `POST` | `/pdf/toImages` | PDF → ZIP с PNG/JPEG. Body: `{ fileId, format, dpi }` |
| `POST` | `/pdf/fromImages` | Изображения → PDF (`multipart/form-data`, поле `images[]`) |
| `GET` | `/pdf/download/:filename` | Скачать результат |
**Парсер статей:**
| Метод | Путь | Описание |
|---|---|---|
| `GET` | `/parse?url=` | Полный разбор статьи: заголовок, автор, текст, HTML, изображения, ссылки |
| `GET` | `/metadata?url=` | Только метаданные (OG, description, дата) |
| `GET` | `/text?url=` | Только текст (`text/plain`) |
| `GET` | `/preview?url=` | Карточка превью: заголовок, описание, изображение |
**HTTP-клиент:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/api/proxy` | Прокси-запрос. Body: `{ url, method, headers, body, timeout }` |
| `GET` | `/api/history` | История запросов текущей сессии |
| `POST` | `/api/history/add` | Добавить запись в историю |
| `DELETE` | `/api/history` | Очистить историю |
**Redirect-анализатор:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/api/redirect-analyze` | Трассировка. Body: `{ url, userAgent: "desktop"|"mobile"|"googlebot", method }` |
**SVG-редактор:**
| Метод | Путь | Описание |
|---|---|---|
| `POST` | `/api/svg-optimize` | Оптимизация SVG. Body: `{ svg: "..." }`. Ответ: `{ svg, saved: % }` |
| `POST` | `/api/svg-ai` | Генерация иконки. Body: `{ keyword, style: "outline"|"filled"|"duotone", size, color }` |
---
## Безопасность
### HTTP-заголовки
Helmet добавляет стандартные security headers. Политика CSP отключена (используется Tailwind CDN с inline-скриптами).
### Rate Limiting (express-rate-limit)
| Эндпоинт | Лимит |
|---|---|
| `/auth/login`, `/auth/register` | 10 запросов / 15 мин |
| `/compress` | Настраивается через `RATE_LIMIT_MAX` / `RATE_LIMIT_WINDOW_MS` (по умолчанию 30 / 60 сек) |
| `/parse`, `/metadata`, `/text`, `/preview` | 20 запросов / 60 сек |
| `/api/proxy` | Настраивается через `PROXY_RATE_LIMIT_MAX` (по умолчанию 60 / 60 сек) |
| `/api/redirect-analyze` | 20 запросов / 60 сек |
| `/video/upload` | 10 запросов / 60 сек |
| `/pdf/*` | 30 запросов / 60 сек |
### SSRF-защита
Все эндпоинты, выполняющие server-side запросы к внешним URL (парсер, HTTP-клиент, redirect-анализатор), проходят валидацию через `lib/ssrf.js`:
- Разрешены только протоколы `http:` и `https:`
- Блокируются прямые обращения к приватным IP-диапазонам: `10.x`, `172.16-31.x`, `192.168.x`, `127.x`, `169.254.x`, IPv6 loopback и link-local
- Блокируются hostname'ы: `localhost`, `*.local`, `*.internal`
- DNS-резолвинг: все A-записи домена проверяются на принадлежность к приватным диапазонам
### Прочее
- Сессионные cookie: `httpOnly`, `secure` (в production), `sameSite: lax`
- Session fixation: при входе сессия перегенерируется через `req.session.regenerate`
- Загружаемые файлы изображений валидируются по MIME-типу и расширению
- Временные файлы автоматически удаляются: загрузки через 10 мин, результаты через 30 мин
- Имена файлов транслитерируются и очищаются от спецсимволов перед записью в архив
---
## Конфигурация
Файл `.env` в корне проекта. Пример структуры:
```env
# Сервер
NODE_ENV=production
PORT=3000
# База данных (MariaDB)
DB_HOST=localhost
DB_USER=wa_tools
DB_PASSWORD=секрет
DB_NAME=wa_tools
# Сессия
SESSION_SECRET=длинная-случайная-строка
# Логирование
LOG_LEVEL=info # error | warn | info | debug
# Загрузка файлов (изображения)
MAX_FILE_SIZE_MB=20 # максимальный размер одного файла
MAX_FILES=50 # максимальное количество файлов в одном запросе
# Сжатие изображений
COMPRESS_QUALITY=80 # качество по умолчанию (1-100)
# Rate limiting (изображения)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=30
# Rate limiting (HTTP-клиент)
PROXY_RATE_LIMIT_MAX=60
```
| Переменная | Описание | По умолчанию |
|---|---|---|
| `NODE_ENV` | Режим работы. В `production` cookie становятся `secure` | `development` |
| `PORT` | Порт Express-сервера | `3000` |
| `DB_HOST` | Хост MariaDB | `localhost` |
| `DB_USER` | Пользователь БД | `wa_tools` |
| `DB_PASSWORD` | Пароль БД | — |
| `DB_NAME` | Имя базы данных | `wa_tools` |
| `SESSION_SECRET` | Секрет для подписи сессионных cookie | `change-me-in-env` |
| `LOG_LEVEL` | Уровень логирования | `info` |
| `MAX_FILE_SIZE_MB` | Лимит размера изображения | `20` |
| `MAX_FILES` | Лимит файлов в пакетной загрузке | `50` |
| `COMPRESS_QUALITY` | Качество сжатия по умолчанию | `80` |
| `RATE_LIMIT_WINDOW_MS` | Окно rate limiting (мс) | `60000` |
| `RATE_LIMIT_MAX` | Лимит запросов в окне | `30` |
| `PROXY_RATE_LIMIT_MAX` | Лимит для HTTP-клиента | `60` |
> **Важно:** Никогда не коммитьте `.env` в репозиторий. Значение `SESSION_SECRET` должно быть случайной строкой длиной не менее 32 символов.
---
## Деплой
### PM2
Конфигурация в `ecosystem.config.js`:
```javascript
{
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,
env: { NODE_ENV: 'production' }
}
```
```bash
# Запуск
pm2 start ecosystem.config.js
# Перезапуск после обновления
pm2 restart images
# Логи
pm2 logs images
```
### Graceful shutdown
Сервер перехватывает `SIGTERM` и `SIGINT`: закрывает HTTP-сервер, ждёт завершения активных соединений до 10 секунд, затем завершается принудительно.
### Nginx
Пример конфигурации виртуального хоста:
```nginx
server {
listen 443 ssl;
server_name images.wadevelop.ru;
ssl_certificate /etc/letsencrypt/live/images.wadevelop.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/images.wadevelop.ru/privkey.pem;
client_max_body_size 210M; # чуть больше MAX видео (200MB)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}
```
В `server.js` установлено `app.set('trust proxy', 1)` — необходимо для корректной работы rate limiting и логирования IP за nginx.
### SSL
```bash
certbot --nginx -d images.wadevelop.ru
```
---
## Мониторинг
### Страница статуса
**URL:** `/status` — публичная HTML-страница в стиле status.claude.com.
**API:** `GET /status/api` — возвращает статус пяти сервисов:
```json
{
"overall": "operational",
"uptime": 86400,
"services": [
{ "name": "API", "status": "operational" },
{ "name": "Database", "status": "operational" },
{ "name": "Image Processing", "status": "operational" },
{ "name": "Video Processing", "status": "operational" },
{ "name": "Authentication", "status": "operational" }
],
"timestamp": "2026-03-22T10:00:00.000Z"
}
```
Статус Database проверяется реальным запросом `SELECT 1` к MariaDB.
### Health Endpoint
**URL:** `GET /health` — расширенная информация о процессе (не требует авторизации):
```json
{
"status": "ok",
"uptime": 3600,
"memory": {
"rss": "128MB",
"heap": "64/128MB"
},
"node": "v22.0.0",
"pid": 12345
}
```
### Логи
Структурированный вывод в stdout (формат: `[ISO timestamp] [LEVEL] message {meta}`). PM2 пишет stdout в `/home/treamz/.pm2/logs/images-out.log`, stderr — в `images-error.log`.
Операции сжатия изображений дополнительно пишутся в файл `compress.log` в корне проекта (через `log.logToFile`).
---
## Технологии
| Компонент | Технология | Назначение |
|---|---|---|
| Runtime | Node.js 18+ | Серверная среда выполнения |
| Framework | Express 5 | HTTP-сервер, маршрутизация |
| База данных | MariaDB 10.6+ | Хранение пользователей, настроек, контента |
| ORM | Sequelize 6 | Модели для AdminJS |
| DB Driver | mysql2 | Прямые SQL-запросы (пул соединений) |
| Авторизация | bcrypt + express-session | Хеширование паролей, серверные сессии |
| Админ-панель | AdminJS 7 + @adminjs/sequelize | Веб-интерфейс управления данными |
| Изображения | Sharp | Конвертация, сжатие, генерация placeholder |
| Видео | FFmpeg + ffprobe | Конвертация, сжатие, извлечение аудио, GIF |
| PDF | pdf-lib | Merge, split, rotate, watermark, protect |
| PDF | pdf-parse | Извлечение текста и метаданных |
| PDF | Ghostscript | Сжатие, конвертация PDF в изображения |
| Парсер | @mozilla/readability + JSDOM | Извлечение контента статей |
| Кодировки | iconv-lite | Декодирование страниц в non-UTF-8 |
| Загрузка файлов | Multer | Обработка multipart/form-data |
| Архивация | Archiver | ZIP-архивы с результатами |
| Геолокация | geoip-lite | Определение страны по IP в логах |
| Безопасность | Helmet | HTTP security headers |
| Rate Limiting | express-rate-limit | Ограничение частоты запросов |
| Шрифты SVG | opentype.js | Работа с шрифтами (вспомогательная) |
| UI | Tailwind CSS (CDN) | Стилизация интерфейса |
| Шрифты UI | JetBrains Mono, Manrope | Типографика |
| Процесс | PM2 | Управление процессом в production |
| Веб-сервер | Nginx | Reverse proxy, SSL termination |
| SSL | Let's Encrypt (certbot) | TLS-сертификаты |
---
## База данных
### Схема таблиц
**`users`** — учётные записи пользователей
| Поле | Тип | Описание |
|---|---|---|
| `id` | INT PK AI | Идентификатор |
| `email` | VARCHAR(255) UNIQUE | Email-адрес |
| `password_hash` | VARCHAR(255) | bcrypt-хеш пароля |
| `display_name` | VARCHAR(100) | Отображаемое имя |
| `role` | ENUM('user','admin') | Роль, по умолчанию `user` |
| `is_blocked` | BOOLEAN | Блокировка аккаунта |
| `created_at` | DATETIME | Дата регистрации |
| `last_login` | DATETIME | Дата последнего входа |
**`settings`** — настройки приложения (key-value)
| Поле | Тип | Описание |
|---|---|---|
| `id` | INT PK AI | Идентификатор |
| `setting_key` | VARCHAR(100) UNIQUE | Ключ настройки |
| `setting_value` | TEXT | Значение |
| `description` | VARCHAR(255) | Описание для AdminJS |
| `updated_at` | DATETIME | Дата обновления |
**`categories`** — категории инструментов
| Поле | Тип | Описание |
|---|---|---|
| `id` | INT PK AI | Идентификатор |
| `slug` | VARCHAR(50) UNIQUE | URL-идентификатор |
| `title` | VARCHAR(100) | Название |
| `description` | VARCHAR(255) | Описание |
| `icon_svg` | TEXT | SVG-иконка категории |
| `color` | VARCHAR(20) | Акцентный цвет (hex) |
| `sort_order` | INT | Порядок отображения |
**`tools`** — инструменты платформы
| Поле | Тип | Описание |
|---|---|---|
| `id` | INT PK AI | Идентификатор |
| `slug` | VARCHAR(50) UNIQUE | URL-идентификатор |
| `title` | VARCHAR(100) | Название |
| `description` | VARCHAR(255) | Описание |
| `path` | VARCHAR(100) | URL-путь инструмента |
| `icon_svg` | TEXT | SVG-иконка |
| `category_id` | INT FK | Ссылка на `categories.id` |
| `sort_order` | INT | Порядок в категории |
| `is_enabled` | BOOLEAN | Включён/отключён |
**`content_blocks`** — текстовые блоки лендинга
| Поле | Тип | Описание |
|---|---|---|
| `id` | INT PK AI | Идентификатор |
| `block_key` | VARCHAR(100) UNIQUE | Уникальный ключ блока |
| `title` | VARCHAR(255) | Заголовок |
| `body` | TEXT | HTML-содержимое (richtext) |
| `section` | VARCHAR(50) | Секция лендинга (`advantages`, `dashboard` и др.) |
| `sort_order` | INT | Порядок в секции |
| `is_visible` | BOOLEAN | Отображать/скрыть |
| `updated_at` | DATETIME | Дата последнего изменения |