# 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-templates` → `public/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.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)` | Клик по элементу в превью → фокус на поле | ### Реактивные вычисления ($:) ```javascript $: 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`) - Оборачивает `предлог + слово` в `` - Использует placeholder-подход чтобы избежать двойной обработки --- ## 7. Гендерная сегментация — техническая реализация ### Данные ```json // 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.svelte** → `apiRenderEmail(..., { gender: 'female'|'male' })` 2. **vite.config.js** → читает `genderPaths` из settings.json, форвардит в email-gen-api 3. **email-gen-api/server.js** → `rewriteHtmlPug(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) ```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`: ```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 - Формат: `{текст} ⠀×130` - `#MAILRU_PREHEADER_TAG#` в pug заменяется на `` в server.js --- ## 11. Деплой ### Сервер - IP: определяется из конфига - Проект: `/opt/va/` (или аналогичный путь) - Nginx: reverse proxy → localhost:6001 ### Обновление кода **Frontend (App.svelte, app.css, lib/*.js):** ```bash # Файлы примонтированы через том — просто редактируй на хосте # HMR подхватит автоматически ``` **Backend (vite.config.js):** ```bash # Файл примонтирован, но Vite не перечитывает конфиг docker compose restart builder ``` **Шаблоны email (email-gen/):** ```bash docker compose build && docker compose up -d ``` **email-gen-api/server.js:** ```bash docker compose build email-gen-api && docker compose up -d email-gen-api ``` ### Первый запуск ```bash git clone 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/`