Full project: Svelte 5 frontend, Vite 7 backend API, Pug email templates (email-gen), Docker deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
28 KiB
Aspekter VA — Руководство разработчика
1. Обзор архитектуры
Aspekter VA — монолитное web-приложение для сборки email-рассылок VipAvenue.
┌─────────────────────────────────────────────────┐
│ Браузер │
│ App.svelte (UI) ←→ api.js (HTTP client) │
└────────────────────┬────────────────────────────┘
│ HTTP
┌────────────────────▼────────────────────────────┐
│ builder (порт 6001) │
│ Vite dev server + API middleware │
│ vite.config.js — весь backend (~1500 строк) │
└────────────────────┬────────────────────────────┘
│ HTTP (внутренний)
┌────────────────────▼────────────────────────────┐
│ email-gen-api (порт 8787) │
│ server.js — Pug → HTML рендер │
│ email-gen/ — шаблоны, стили, миксины │
└─────────────────────────────────────────────────┘
Стек
- Frontend: Svelte 5 + Vite 7
- Backend: Vite middleware plugin (Node.js, встроен в dev-server)
- Email рендер: Pug шаблоны + email-templates (npm)
- Хранение данных: файловая система (JSON)
- Инфраструктура: Docker Compose, nginx reverse proxy
Почему так устроено
Backend встроен как Vite middleware plugin, а не как отдельный сервер. Это значит:
- Один процесс, один порт
- HMR для фронтенда работает из коробки
- Нет CORS-проблем
- Но: vite.config.js не перезагружается через HMR — нужен
docker restart builder
2. Структура файлов
VA.ASPEKTER/
├── docker-compose.yml # Оркестрация контейнеров
├── deploy/
│ ├── nginx/ # Nginx конфиг + лендинг
│ │ ├── nginx.conf
│ │ └── landing.html
│ └── email-gen-api/
│ ├── Dockerfile
│ └── server.js # Standalone Pug render service
├── email-gen/ # Git-репозиторий коллег (шаблоны)
│ └── emails/vipavenue/
│ ├── layout/layout.pug # Базовый layout (width=600)
│ ├── blocks/block.pug # Все блоки конструктора
│ ├── includes/mixins.pug # Pug миксины (товары, кнопки, preheader)
│ ├── css/style.css # Стили (инлайнятся при рендере)
│ └── parts/ # Хедеры, футеры (по гендеру)
│ ├── header/
│ └── footer/
└── z51-pug-builder/ # Основное приложение
├── Dockerfile
├── package.json
├── vite.config.js # ВЕСЬ BACKEND API (~1500 строк)
├── index.html # Входная точка Vite
├── public/ # Статика (favicon)
├── src/
│ ├── App.svelte # ВЕСЬ FRONTEND UI (~6400 строк)
│ ├── app.css # Все стили
│ ├── main.js # Точка входа Svelte
│ └── lib/
│ ├── api.js # API-клиент (все эндпоинты)
│ ├── parsing.js # Парсинг Pug-блоков, миксинов, аргументов
│ ├── spellcheck.js # Проверка орфографии (Yandex Speller)
│ └── utils.js # Утилиты (normalizeNewlines, escapeRegExp, etc)
└── data/ # Данные (том Docker)
├── config.json # Глобальный конфиг (Yonote токен, URL)
├── feed-cache.json # Кэш товарного фида
├── render-cache.json # Кэш рендера (Pug→HTML)
├── _system/
│ ├── users.json # Пользователи (логин, хэш пароля, роль)
│ └── sessions.json # Активные сессии
└── vipavenue/ # Данные проекта
├── settings.json # Настройки (блоки, FTP, гендер, фид)
├── block.pug # Текущий пуг-файл конструктора
├── presets.json # Пресеты
├── notes.json # Заметки (индекс)
├── notes/ # Файлы заметок
├── stats.json # Статистика времени
├── drafts/ # Черновики по пользователям
└── letters/ # Карточки писем по пользователям
└── {userId}/
├── letters.json # Индекс писем
├── {id}.json # Данные письма
└── {id}.history.json # История снимков
3. Docker — контейнеры и тома
Контейнеры
| Контейнер | Образ | Порт | Назначение |
|---|---|---|---|
builder |
z51-pug-builder/Dockerfile | 6001→5173 | Vite dev server + API |
email-gen-api |
deploy/email-gen-api/Dockerfile | (внутренний) 8787 | Pug→HTML рендер |
| nginx | (отдельно) | 80/443 | Reverse proxy |
Тома builder
| Хост | Контейнер | Режим | HMR? |
|---|---|---|---|
./z51-pug-builder/src |
/app/src |
rw | ✅ Мгновенно |
./z51-pug-builder/vite.config.js |
/app/vite.config.js |
rw | ❌ Нужен restart |
./z51-pug-builder/data |
/app/data |
rw | — |
./email-gen |
/email-gen |
rw | ❌ Нужен build |
Тома email-gen-api
| Хост | Контейнер |
|---|---|
./email-gen |
/workspace/email-gen (rw) |
| Named volume | /workspace/email-gen/node_modules |
Правила пересборки (КРИТИЧЕСКИ ВАЖНО)
docker compose build && docker compose up -d нужен при:
- Изменения в
email-gen/(pug шаблоны, миксины, стили) - Изменения в
deploy/email-gen-api/server.js - Изменения в
z51-pug-builder/Dockerfile - Изменения в
z51-pug-builder/package.json(новые зависимости)
docker compose restart builder нужен при:
- Изменения в
z51-pug-builder/vite.config.js
Ничего не нужно (HMR подхватит):
- Изменения в
z51-pug-builder/src/(App.svelte, app.css, lib/*.js)
НЕ использовать
docker cpилиdocker execдля правки исходников на проде. Только редактировать на хосте, тома примонтированы.
4. Backend API — все эндпоинты
Весь API реализован в vite.config.js как Vite middleware plugin.
Авторизация (без аутентификации)
| Метод | URL | Описание |
|---|---|---|
| POST | /api/auth/login |
Вход: {login, password} → cookie va_token |
| POST | /api/auth/logout |
Выход: удаляет cookie |
| GET | /api/auth/me |
Текущий пользователь (или 401) |
| PUT | /api/auth/preferences |
Сохранить тему/activePage/zoom |
Администрирование (role: admin)
| Метод | URL | Описание |
|---|---|---|
| GET | /api/admin/users |
Список пользователей |
| POST | /api/admin/users |
Создать: {login, password, name, role} |
| PUT | /api/admin/users/:id |
Обновить пользователя |
| DELETE | /api/admin/users/:id |
Удалить (нельзя удалить себя) |
Проект (все требуют аутентификации)
| Метод | URL | Описание |
|---|---|---|
| GET | /api/projects |
Список проектов (всегда ["vipavenue"]) |
| GET | /api/project/:name |
Данные проекта (блок, настройки, черновик, пресеты, письма, заметки) |
| PUT | /api/project/:name/block |
Сохранить block.pug |
| PUT | /api/project/:name/block-custom |
Сохранить кастомный блок |
| PUT | /api/project/:name/settings |
Сохранить настройки |
| PUT | /api/project/:name/draft |
Сохранить черновик |
| PUT | /api/project/:name/presets |
Сохранить пресеты |
Письма
| Метод | URL | Описание |
|---|---|---|
| GET | /api/project/:name/letter/:id |
Получить письмо |
| PUT | /api/project/:name/letter |
Сохранить: {id, ...data} |
| DELETE | /api/project/:name/letter/:id |
Удалить |
| GET | /api/project/:name/letter/:id/history |
История снимков |
| PUT | /api/project/:name/letter/:id/history |
Добавить снимок |
Заметки
| Метод | URL | Описание |
|---|---|---|
| GET | /api/project/:name/notes |
Индекс заметок |
| PUT | /api/project/:name/notes |
Сохранить индекс |
| GET | /api/project/:name/note/:id |
Получить заметку |
| PUT | /api/project/:name/note |
Сохранить: {id, ...data} |
| DELETE | /api/project/:name/note/:id |
Удалить |
Рендер email
| Метод | URL | Описание |
|---|---|---|
| POST | /api/project/:name/render-email |
Рендер Pug→HTML. Body: {projectSlug, pug, preheader, gender} |
Цепочка рендера:
- Frontend отправляет Pug-код + preheader + gender
- Backend формирует hash, проверяет кэш
- Если кэш-промах → отправляет в email-gen-api
- email-gen-api: записывает
letters/let.pug, генерируетhtml.pug, запускаетemail-templates→public/index.html - Backend читает HTML, обрабатывает Mindbox-теги (подставляет товары), применяет nowrap
- Возвращает
{html, previewHtml, unavailableProducts}
Товарный фид
| Метод | URL | Описание |
|---|---|---|
| POST | /api/project/:name/feed-refresh |
Очистить кэш фида, перезагрузить |
| POST | /api/project/:name/feed-lookup |
Найти товары по ID: {ids: [...]} |
| POST | /api/project/:name/feed-suggest |
Подобрать замену: {productId, excludeIds, search} |
Фид кэшируется на 3 часа. XML парсится regex-ом (не DOM), поддерживается windows-1251.
FTP/SFTP
| Метод | URL | Описание |
|---|---|---|
| POST | /api/project/:name/ftp/test |
Проверить подключение |
| POST | /api/project/:name/ftp/upload |
Загрузить файл: {imageData, fileName, folder} |
| POST | /api/project/:name/ftp/list |
Список файлов: {folder} |
| POST | /api/project/:name/ftp/delete |
Удалить файл: {folder, fileName} |
Прочее
| Метод | URL | Описание |
|---|---|---|
| GET | /api/config |
Глобальный конфиг (Yonote, upload URL) |
| PUT | /api/config |
Сохранить конфиг |
| GET | /api/yonote/status |
Статус подключения Yonote |
| GET | /api/yonote/databases |
Список баз Yonote |
| GET | /api/yonote/database/:id/properties |
Свойства базы |
| GET | /api/yonote/database/:id/rows |
Строки базы |
| POST | /api/yonote/row/update |
Обновить строку: {rowId, values} |
| POST | /api/upload-image |
Загрузить изображение: {imageData, fileName} |
| POST | /api/check-links |
Проверить ссылки: {urls: [...]} |
| GET | /api/parts-files |
Список pug-файлов из parts/ |
| GET | /api/parts-file-read?path=... |
Прочитать parts-файл |
| POST | /api/parts-file-write |
Записать parts-файл |
| GET | /api/project/:name/stats |
Статистика проекта |
| POST | /api/project/:name/stats |
Сохранить запись статистики |
| GET | /api/stats |
Вся статистика (все проекты) |
5. Frontend — архитектура
Общая структура
Весь UI — один файл App.svelte (~6400 строк). Это монолит. Причина — проект начинался как прототип и вырос.
Ключевые модули (lib/)
api.js — HTTP-клиент. Все функции api*() вызывают apiRequest() который делает fetch() с Content-Type: application/json и обрабатывает ошибки.
parsing.js — парсинг Pug-кода:
parseBlocks(text)— разбивает Pug на блоки по//комментариямbuildBaseSchema(content, blockName, mixinRules)— строит схему полей блока (какие поля редактируемые)parseSections(content)— разбивает блок на секции для drag-n-dropfindMixinArgRange(line, argIndex)— находит позицию аргумента миксина в строкеextractMixinArgValue()/replaceMixinArgQuoted()— чтение/запись аргументов
spellcheck.js — checkSpelling(text) отправляет в Yandex Speller, injectSpellMarks(html, errors) подсвечивает ошибки в превью.
utils.js — normalizeNewlines(), escapeRegExp(), unquoteValue(), downloadFile().
State management
Svelte 5 reactive let переменные. Ключевые группы:
| Группа | Переменные | Описание |
|---|---|---|
| UI | activePage, theme, sidebarTab, settingsBlockIndex |
Состояние интерфейса |
| Данные | assembledBlocks, presets, planRows, allStats |
Основные данные |
| Рендер | renderedHtml, previewHtml, autoRenderTimer |
Результат рендера |
| Гендер | currentGenderVersion, segmentFlipped |
Гендерная сегментация |
| Фид | feedProducts, unavailableProducts |
Товары |
| Письма | currentLetterId, letterName |
Текущее письмо |
| Настройки | settings |
settings.json проекта |
Ключевые функции
| Функция | Назначение |
|---|---|
loadProject() |
Загрузка проекта при старте |
rebuildOutput() |
Пересборка Pug из блоков → рендер |
renderEmail() |
Отправка Pug на сервер для рендера |
assembleGenderVersion(target) |
Сборка гендерной версии |
flipSegmentOrder() |
Переключение порядка блоков (⇅) |
addBlock(name) / removeBlock(id) |
Управление блоками |
updateBlockContent(id, content) |
Обновление содержимого блока |
saveDraft() |
Автосохранение черновика |
copyHtml() |
Копирование HTML в буфер |
handlePreviewElementClick(data) |
Клик по элементу в превью → фокус на поле |
Реактивные вычисления ($:)
$: settingsBlockName = allBlocks[settingsBlockIndex]?.name
$: settingsTemplate = getEffectiveTemplate(settingsBlockName)
$: baseSchema = buildBaseSchema(settingsTemplate, settingsBlockName, ...)
$: allBlocks = [...blockTemplates, ...customBlocksList]
$: productOptionsList = (settings.productOptions || []).map(...)
6. Email rendering pipeline
Полная цепочка
1. App.svelte: assembleGenderVersion() + rebuildOutput()
↓ Собирает Pug из блоков
2. App.svelte: apiRenderEmail(name, {projectSlug, pug, preheader, gender})
↓ HTTP POST
3. vite.config.js: /api/project/:name/render-email
↓ pugHash() → проверка кэша
↓ Если промах:
4. vite.config.js → HTTP POST email-gen-api:8787/render
↓ {projectSlug, pug, preheader, gender, genderPaths}
5. email-gen-api/server.js:
a) Записывает letters/let.pug (содержимое из конструктора)
b) rewriteHtmlPug() — генерирует html.pug с правильным header/footer
c) renderWithNode() — вызывает email-templates (Pug → HTML)
d) Читает public/index.html → возвращает HTML
6. vite.config.js:
a) Кэширует HTML
b) processMindboxTags() — подставляет товары из фида
c) applyNowrap() — склеивает предлоги с nowrap-спанами
d) Возвращает {html, previewHtml, unavailableProducts}
7. App.svelte: показывает превью в iframe
Кэш рендера
- Ключ:
md5(slug + pug + gender + genderPaths) - Максимум 30 записей (LRU)
- Персистится в
data/render-cache.json - Инвалидируется при смене Pug-кода, гендера или путей header/footer
Mindbox-обработка
- Замена
@{for...}@{end for}блоков - Извлечение ID товаров из
GetByValue('12345') - Подстановка данных из фида (цена, название, картинка, URL)
- Отслеживание недоступных товаров
Nowrap-обработка
- Только для текстовых блоков (span с классом
h3) - Оборачивает
предлог + словов<span style="white-space:nowrap"> - Использует placeholder-подход чтобы избежать двойной обработки
7. Гендерная сегментация — техническая реализация
Данные
// settings.json → genderPaths
{
"headerFemale": "./parts/header/header-woman",
"headerMale": "./parts/header/header-man",
"footerFemale": "./parts/footer/footer-woman",
"footerMale": "./parts/footer/footer-man"
}
Цепочка передачи gender
- App.svelte →
apiRenderEmail(..., { gender: 'female'|'male' }) - vite.config.js → читает
genderPathsиз settings.json, форвардит в email-gen-api - email-gen-api/server.js →
rewriteHtmlPug(projectDir, preheader, gender, genderPaths)— записываетhtml.pugс нужным include header/footer - Pug рендерит HTML с правильным header/footer
Кэш
pugHash(slug, pug, gender, genderPaths) — gender и genderPaths включены в ключ, female/male кэшируются отдельно.
Flip (⇅)
flipSegmentOrder()в App.svelte- Находит блок-разделитель с
swapCenter = true - Блоки до разделителя и после меняются местами
segmentFlipped(bool) отслеживает состояние toggle
8. Авторизация и безопасность
Аутентификация
- Хэширование: scrypt (salt 16 bytes, key length 64)
- Сессии: случайный токен 32 bytes hex, хранится в Map + файл sessions.json
- Cookie:
va_token, HttpOnly, SameSite=Strict, Secure, Max-Age=7d - Brute-force: 5 попыток на IP за 15 минут
Middleware стек (порядок важен!)
- Security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)
- Auth endpoints (/api/auth/*) — без проверки сессии
- CSRF проверка (origin/referer vs host)
- Auth middleware — проверка cookie → req.user
- Admin middleware (/api/admin/*) — проверка role=admin
- Uploads middleware (/uploads/*) — статика с проверкой path traversal
- API middleware — основные эндпоинты
Защита от атак
- Path traversal:
sanitizeFileId()для всех файловых ID,getProjectDir()проверяет startsWith - SSRF: фильтрация приватных IP (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16)
- CSRF: проверка origin/referer на state-changing запросах
- XSS: не применимо (внутренний инструмент, HTML генерируется из шаблонов)
9. Настройки проекта (settings.json)
{
"globalSpacing": 40,
"blocks": {
"Текст": { "spacing": 20, "template": "..." },
"Кнопка": { "spacing": 10 }
},
"genderPaths": {
"headerFemale": "./parts/header/header-woman",
"headerMale": "./parts/header/header-man",
"footerFemale": "./parts/footer/footer-woman",
"footerMale": "./parts/footer/footer-man"
},
"feedUrl": "https://...",
"ftpConfig": {
"host": "...",
"port": 21,
"user": "...",
"password": "***",
"protocol": "ftp",
"remotePath": "/images",
"publicUrl": "https://..."
},
"mixinRules": [
{ "mixin": "buttonRounded", "argIndex": 0, "type": "mixin-text", "label": "Текст кнопки" }
],
"productOptions": [
{ "code": "showPrice:true", "label": "Показывать цену", "defaultEnabled": true }
],
"customBlocks": [
{ "id": "...", "name": "Мой блок", "content": "..." }
]
}
10. email-gen — шаблоны (репозиторий коллег)
Структура
email-gen/emails/vipavenue/
├── layout/layout.pug # Основной layout, table width=600
├── blocks/block.pug # Все блоки (//Текст, //Кнопка, //Товары, etc)
├── includes/mixins.pug # Pug-миксины (+products, +buttonRounded, +preheader, etc)
├── css/style.css # CSS (инлайнится при рендере)
└── parts/
├── header/ # Хедеры (header-woman.pug, header-man.pug, header-dark.pug)
└── footer/ # Футеры (аналогично)
Как добавить новый блок
- Добавить секцию в
blocks/block.pug://Мой новый блок tr td.paddingWrapper +defaultTable("100%") tr td(align="center") span.font.h3.blackText Текст блока - Пересобрать контейнеры:
docker compose build && docker compose up -d - Блок появится в конструкторе
Preheader
- Миксин
+preheader(text)в mixins.pug - Формат:
{текст} <vk-snippet-end>⠀×130 #MAILRU_PREHEADER_TAG#в pug заменяется на<vk-snippet-end>в server.js
11. Деплой
Сервер
- IP: определяется из конфига
- Проект:
/opt/va/(или аналогичный путь) - Nginx: reverse proxy → localhost:6001
Обновление кода
Frontend (App.svelte, app.css, lib/*.js):
# Файлы примонтированы через том — просто редактируй на хосте
# HMR подхватит автоматически
Backend (vite.config.js):
# Файл примонтирован, но Vite не перечитывает конфиг
docker compose restart builder
Шаблоны email (email-gen/):
docker compose build && docker compose up -d
email-gen-api/server.js:
docker compose build email-gen-api && docker compose up -d email-gen-api
Первый запуск
git clone <repo>
cd VA.ASPEKTER
docker compose build
docker compose up -d
# Смотреть логи для временного пароля admin:
docker compose logs builder | grep "Временный пароль"
12. Известные особенности и ограничения
Архитектурные
- Монолит: App.svelte ~6400 строк, vite.config.js ~1500 строк. Декомпозиция не проводилась.
- Файловое хранилище: нет БД, всё в JSON. Race condition при параллельных записях теоретически возможен (writeFileSync без блокировки).
- Single-project: код спроектирован под vipavenue. Мультипроектность убрана, но следы остались (PROJECT_NAME constant).
Рендер
- Кэш: 30 записей LRU. При изменении CSS в email-gen кэш не инвалидируется (нужно очистить вручную или пересобрать).
- Mindbox-теги: парсятся regex-ом, не DOM. Если формат тегов изменится — сломается.
- XML-фид: парсится regex-ом. CDATA, вложенные теги с одинаковым именем — потенциально проблемны.
Фронтенд
- Svelte 5: используется Svelte 5, но не все runes-паттерны применены.
ensureSchema()мутирует block.schema напрямую. - MutationObserver: на document.body с subtree=true. Debounce 200ms добавлен, но это всё равно потенциальная нагрузка.
TODO (отмечено в коде)
- Google Sheets → Yonote API миграция (поиск по
TODO: YONOTE) - Пользовательские настройки (тема, zoom) частично перенесены на сервер, localStorage ещё используется как fallback
- Pull email-gen из git — функция запланирована, но не реализована
13. Conventions
Именование
- localStorage ключи:
va-*(va-active-page, va-theme, va-plan-cache, va-previewZoom) - Cookie:
va_token - CSS классы:
va-spell(spellcheck),vaclick(preview detection) - Docker:
vaaspekter-builder-1,vaaspekter-email-gen-api-1
Форматирование кода
- 2 пробела отступ
- Одинарные кавычки в JS
- Без точек с запятой (semicolons) — inconsistent, в некоторых местах есть
Git
email-gen/— это отдельный git-репозиторий коллег, вложен как поддиректория (НЕ submodule)- Основной репозиторий:
VA.ASPEKTER/