# Устройство кода ENTHELOS

Этот документ — карта проекта для разработчика. Запуск, SMTP, размещение и резервные копии описаны в [BACKEND.md](BACKEND.md); замена ссылок eKliinik — в [BOOKING.md](BOOKING.md).

## Структура проекта

| Путь                                | Назначение                                                                     |
| ----------------------------------- | ------------------------------------------------------------------------------ |
| `public/`                           | Публичная вёрстка. Русские HTML-файлы — редактируемый оригинал                 |
| `public/et/`, `public/en/`          | Сгенерированные переводы тех же страниц                                        |
| `public/style.css`                  | Основное оформление, адаптивность, анимации                                    |
| `public/blog.css`, `public/blog.js` | Карточки статей, оформление публикаций, управление каруселями                  |
| `public/contact.js`                 | Отправка формы и сообщения на трёх языках                                      |
| `public/assets/`                    | Изображения и локальные шрифты                                                 |
| `admin/index.html`                  | Разметка входа и редактора блога                                               |
| `admin/admin.js`                    | Состояние редактора, Quill, загрузки, сохранение и предпросмотр                |
| `admin/admin.css`                   | Стили админки, независимые от оформления публичных страниц                     |
| `server/index.js`                   | Создание Express-приложения, middleware, API, рендеринг блога и раздача файлов |
| `server/blog-rendering.js`          | Читаемые HTML-шаблоны списка публикаций и статьи                               |
| `server/content.js`                 | Проверка статьи, очистка HTML и рендеринг медиаблоков                          |
| `server/store.js`                   | SQLite-хранилище статей и адаптер сессий                                       |
| `server/password.js`                | Генерация пароля и запись его scrypt-хеша в `.env`                             |
| `server/test/app.test.js`           | Интеграционные тесты API, базы, загрузок и SMTP                                |
| `localize.py`                       | Генератор ET/EN с проверкой полноты переводов                                  |
| `locales/`                          | Русский словарь и переводы с устойчивыми числовыми ID                          |
| `data/`                             | Рабочая база и загрузки; не исходный код, не включается в поставку             |

`node_modules`, SQLite, изображения, шрифты, lockfile и секреты не требуют ручного форматирования. Код стороннего редактора не изменяется; Quill обслуживается из установленного npm-пакета локально.

## Запрос посетителя

1. Node обслуживает существующие страницы из `public`.
2. Маршруты блога обрабатываются раньше статических файлов. При отсутствии опубликованных статей остаётся исходная страница-заглушка.
3. Для списка и статьи сервер берёт шапку/подвал из `public/[язык]/blog/index.html`, заменяя только `<main>` и заголовок документа. Регулярные выражения допускают переносы строк в HTML.
4. В публичной выборке обязательны `status === 'published'` и совпадение языка. Контент статьи очищается при сохранении, подписи и URL экранируются при рендеринге.

Порядок middleware имеет значение: заголовки безопасности → JSON → сессия → проверка доступа → обработчики → статические файлы → 404 → обработчик ошибок. Не переносите загрузки до проверки доступа.

## Модель статьи

```json
{
  "id": "UUID",
  "title": "Заголовок",
  "slug": "article-address",
  "lang": "ru",
  "excerpt": "Текст карточки",
  "cover": "/uploads/UUID.webp",
  "status": "draft",
  "blocks": [
    { "type": "text", "html": "<p>Текст статьи</p>" },
    {
      "type": "gallery",
      "layout": "carousel",
      "items": [{ "url": "/uploads/UUID.webp", "alt": "Описание", "caption": "Подпись" }]
    }
  ],
  "createdAt": "ISO date",
  "updatedAt": "ISO date",
  "publishedAt": null
}
```

`lang`: `ru`, `et` или `en`. `status`: `draft` или `published`. `layout`: `grid` или `carousel`. `cover` может быть пустой строкой. При первом опубликовании устанавливается `publishedAt`. При обновлении отправляется полученный `updatedAt`: устаревшая версия отклоняется с HTTP 409.

Имена полей JSON, URL API, ID формы и схема SQLite сохранены при рефакторинге: уже созданные статьи и загрузки совместимы. Не переименовывайте эти поля без миграции. Пара `(lang, slug)` уникальна на уровне обработчика сохранения.

## API

| Метод и путь                  | Назначение                                   | Доступ                           |
| ----------------------------- | -------------------------------------------- | -------------------------------- |
| `GET /api/admin/session`      | Состояние входа и CSRF-токен                 | Публичный                        |
| `POST /api/admin/login`       | Вход по `{password}`                         | Совпадение Origin, лимит попыток |
| `POST /api/admin/logout`      | Завершение сессии                            | Сессия + Origin + CSRF           |
| `GET /api/admin/posts`        | Все статьи редактора                         | Сессия                           |
| `PUT /api/admin/posts/:id`    | Создание/обновление статьи                   | Сессия + Origin + CSRF           |
| `DELETE /api/admin/posts/:id` | Удаление статьи                              | Сессия + Origin + CSRF           |
| `POST /api/admin/upload`      | Multipart, поле `file`                       | Сессия + Origin + CSRF           |
| `POST /api/contact`           | `{phone, email, description, lang, website}` | Origin, проверка данных, лимит   |

CSRF передаётся заголовком `X-CSRF-Token`. Браузер отправляет cookie сессии автоматически. Поле `website` в форме — скрытая ловушка для ботов. Ошибка API имеет поле `error`; сообщения формы локализуются клиентом по кодам `validation`, `rate`, `unavailable`, `delivery`.

Запросы отправки заявки не сохраняются в SQLite и не выводятся в лог. SMTP-секреты читаются только сервером. Тесты используют временную базу и локальный SMTP, а не рабочий почтовый ящик.

## Работа редактора

`currentPost` — открытая статья, `blocks` — модели видимых блоков, `hasUnsavedChanges` — признак правок. `renderBlocks` создаёт элементы редактора; `serializeBlocks` извлекает обычный JSON, не сохраняя экземпляры Quill. `runEditorAction` предотвращает одновременное выполнение действий сохранения. `requestJson` добавляет CSRF-токен и разбирает ошибки.

Для загрузок передаётся `FormData`: не задавайте вручную `Content-Type`, иначе потеряется multipart boundary. Сервер проверяет сигнатуру файла, а не доверяет имени. При удалении статьи файлы остаются: они могут использоваться в других публикациях.

## HTML, CSS и названия

Имена CSS — `kebab-case`, функции и переменные JavaScript — `camelCase`, классы JavaScript — `PascalCase`, Python — `snake_case`, константы Python — `UPPER_SNAKE_CASE`. `req`, `res` и `next` оставлены как общепринятые имена Express. Классы Quill с префиксом `ql-` принадлежат библиотеке.

| Класс                                                                       | Назначение                                       |
| --------------------------------------------------------------------------- | ------------------------------------------------ |
| `service-grid`, `service-card`, `service-card-body`                         | Сетка услуг, карточка, текст карточки            |
| `card-photo`, `photo-consultation`, `photo-in-person`, `photo-home-support` | Рамка фото и индивидуальное кадрирование         |
| `specialist-card`, `specialist-details`, `specialist-role`                  | Карточка специалиста, описание, должность        |
| `article-content`                                                           | Текстовая часть информационных страниц           |
| `blog-cards`, `blog-article`, `article-text`                                | Список публикаций, статья, форматированный текст |
| `media-gallery`, `grid`, `carousel`                                         | Галерея и варианты размещения                    |
| `editor-panel`, `form-columns`, `field-hint`                                | Область редактора, колонки формы, подсказка      |
| `button-primary`, `button-danger`                                           | Основное и удаляющее действия админки            |

Белые поля некоторых исходных фотографий находятся внутри файлов: CSS обрезает их внутри `card-photo`. Не убирайте индивидуальные коэффициенты кадрирования без проверки на телефоне. `hero.webp` используется только фоном главной страницы. Повторение логотипа и декоративных фонов намеренное.

Комментарии объясняют ограничения, причины и порядок действий; очевидные присваивания не комментируются. Публичные HTML-файлы организованы по смысловым `header`, `main`, `section`, `footer`; им не нужны комментарии на каждой строке.

## Изменение текстов и переводов

1. Отредактируйте русскую страницу в `public/`.
2. Для нового текста добавьте строку в `locales/source-ru.json` и запись с тем же индексом в `translations-*.txt`: `ID|эстонский|английский`. Не перенумеровывайте существующие записи.
3. Выполните `npm run localize` (нужен Python 3 в PATH). Генератор сверяет тексты без учёта переносов строк, сохраняет inline-разметку и меняет внутренние ссылки на языковые.
4. Проверьте страницу на трёх языках. Медицинские формулировки согласуйте по правилам из `TRANSLATION_NOTES.md`.

ET/EN не редактируются вручную: генератор перезапишет их. Записи блога хранятся в базе и не проходят через `localize.py`; их языковые версии создаются в админке.

## Форматирование и проверка

```sh
npm run format
npm run format:check
npm test
npm run check
```

Prettier использует `.prettierrc.json`; `.editorconfig` задаёт UTF-8, LF и отступы. Python оформлен четырьмя пробелами; Prettier его не обрабатывает. Не минифицируйте редактируемые HTML/CSS/JS. Если понадобится минификация, создайте отдельный выходной каталог сборки.

После правок в `server/` перезапустите Node. Статические файлы читаются без перезапуска. Для приёмки проверьте главную, услуги, специалистов, контакты и админку на 390, 941 и 1440 px. После изменений HTML дополнительно выполните локализацию и повторное форматирование.

## Подготовка public

`npm run build` запускает существующий генератор переводов и форматирует HTML в `public/`. Это не Vite и не SPA: русские HTML и assets в `public/` являются исходниками. Не удаляйте `public/` перед сборкой. Команда `npm run localize` делает то же самое. Нужны установленные devDependencies и Python 3; нестандартный путь к Python можно задать переменной `PYTHON`. Скрипт `scripts/build.mjs` определяет корень проекта относительно себя.

## Разделы админки и ссылки записи

После входа показывается выбор раздела: блог или ссылки eKliinik. Разделы не отображаются одновременно; состояние несохранённого текста сохраняется при переходе на экран выбора. Ссылки редактируются отдельной формой с независимым состоянием.

`server/booking-links.js` читает исходные ссылки из страниц контактов и валидирует новые адреса. Таблица SQLite `settings` добавляется автоматически и хранит запись `booking-links` с полями `links: {ru, et, en}` и `revision`. Статьи, сессии и их схемы сохранены. Сохранение защищено существующими middleware авторизации, Origin и CSRF.

`GET /api/booking-links` отдаёт только публичные URL. `GET/PUT /api/admin/booking-links` доступны администратору. Контактные страницы получают актуальную ссылку в HTML; `public/booking-links.js` обновляет её также при прямой выдаче HTML Apache. Подробности в BOOKING.md.

## SEO и consent

`server/seo.js` устанавливает middleware до маршрутов и обогащает HTML, выдаёт robots/sitemap и публичный конфиг аналитики. `server/seo-descriptions.js` хранит редакционные описания исключений; остальные выводятся из текста страницы. `scripts/build-seo.mjs` обрабатывает физические HTML после localize. Метки SEO:START/SEO:END позволяют повторять сборку без дубликатов. `public/analytics-consent.js` загружает GA4 только после согласия, вне страниц с формами. Полная инструкция подключения — SEO.md.
