Files
VA.ASPEKTER/docs/02-developer-guide.md
Sergey Zotov c090bfcf47 Initial commit — Aspekter VA email builder
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>
2026-04-13 01:21:00 +05:00

28 KiB
Raw Blame History

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}

Цепочка рендера:

  1. Frontend отправляет Pug-код + preheader + gender
  2. Backend формирует hash, проверяет кэш
  3. Если кэш-промах → отправляет в email-gen-api
  4. email-gen-api: записывает letters/let.pug, генерирует html.pug, запускает email-templatespublic/index.html
  5. Backend читает HTML, обрабатывает Mindbox-теги (подставляет товары), применяет nowrap
  6. Возвращает {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-drop
  • findMixinArgRange(line, argIndex) — находит позицию аргумента миксина в строке
  • extractMixinArgValue() / replaceMixinArgQuoted() — чтение/запись аргументов

spellcheck.jscheckSpelling(text) отправляет в Yandex Speller, injectSpellMarks(html, errors) подсвечивает ошибки в превью.

utils.jsnormalizeNewlines(), 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

  1. App.svelteapiRenderEmail(..., { gender: 'female'|'male' })
  2. vite.config.js → читает genderPaths из settings.json, форвардит в email-gen-api
  3. email-gen-api/server.jsrewriteHtmlPug(projectDir, preheader, gender, genderPaths) — записывает html.pug с нужным include header/footer
  4. 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 стек (порядок важен!)

  1. Security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)
  2. Auth endpoints (/api/auth/*) — без проверки сессии
  3. CSRF проверка (origin/referer vs host)
  4. Auth middleware — проверка cookie → req.user
  5. Admin middleware (/api/admin/*) — проверка role=admin
  6. Uploads middleware (/uploads/*) — статика с проверкой path traversal
  7. 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/             # Футеры (аналогично)

Как добавить новый блок

  1. Добавить секцию в blocks/block.pug:
    //Мой новый блок
    tr
      td.paddingWrapper
        +defaultTable("100%")
          tr
            td(align="center")
              span.font.h3.blackText Текст блока
    
  2. Пересобрать контейнеры: docker compose build && docker compose up -d
  3. Блок появится в конструкторе

Preheader

  • Миксин +preheader(text) в mixins.pug
  • Формат: {текст} <vk-snippet-end>&#10240;×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/