# 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 (default-src self, object-src none, frame-ancestors none). Tailwind собирается локально через CLI.

### 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 3 (CLI build) | Стилизация интерфейса |
| Шрифты 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 | Дата последнего изменения |
