Add project documentation (README.md)

This commit is contained in:
treamz 2026-03-22 01:19:23 +03:00
parent a7d6287d65
commit 75d6c186aa

652
README.md Normal file
View File

@ -0,0 +1,652 @@
# 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 | Дата последнего изменения |