# 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`)
- Оборачивает `предлог + слово` в `<span style="white-space:nowrap">`
- Использует 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
- Формат: `{текст} <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):**
```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 <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/`
