Этап 1a: фундамент

- Next 16.4 и Payload 3.91 в одном приложении, PostgreSQL с UUID,
  TypeScript 6, ESLint 9, Prettier, Vitest
- Payload: администраторы с ролями, multi-tenant по сайтам, локализация
  en/ru/de/fr/es, интерфейс админки ru/en, языки сайта (sites.locales)
- Коллекции sites, domains, currencies, countries, locations, tags,
  tariffs, payment-systems (manual), media и начальные данные (pnpm seed)
- Proxy: хост → домен, язык и валюта, внутренние маршруты по хосту;
  /admin и /api закрыты на доменах брендов
- Токены и темы брендов, оболочки сайта и кабинета, переключатели языка,
  валюты и темы, заглушки страниц
- /svc/health, юнит systemd veilix-dev.service
- Документация: решения D-022…D-024, команды в CLAUDE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreypr207andClaude Opus 5.5 committed 2026-10-21 05:08:05 +02:00
1 parent 3400f58a5a
commit 7e331c2c9d
140 files changed
+15885 -43

No files matched your search

+2 -2
View File
@@ -24,7 +24,7 @@
| Этап | Содержание | Статус |
|---|---|---|
| [0](stages/stage-0-docs.md) | Документация | готово |
| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | не начат |
| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | готово |
| [1b](stages/stage-1b-marketing.md) | Маркетинговые страницы обоих брендов, контент, формы с ALTCHA, SEO, юр. тексты | не начат |
| [1c](stages/stage-1c-auth.md) | Вход по коду, почта, сессии | не начат |
| [1d](stages/stage-1d-billing.md) | Биллинг без эквайринга, оформление заказа, продления | не начат |
@@ -64,7 +64,7 @@
| № | Вопрос | Предложение | Нужно к этапу |
|---|---|---|---|
| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | — нужны цифры | 1b |
| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | Нужны цифры. Пока в seed временные $6.99 и $69.90 (около $1 за 100 ₽, как у dedicated в макете) | 1b |
| Q-08 | Email поддержки брендов | `help@veilix.app` и `help@veilixvpn.com` (в макете были домены .cc) | 1b |
| Q-09 | Часовой пояс часов поддержки (10–20 в рабочие дни) | МСК | 1b |
| Q-10 | Имена Telegram-ботов: 2 основных, 2 поддержки, тестовые | — | 3 |
+30 -18
View File
@@ -11,19 +11,22 @@
| Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) |
| PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` |
| Tailwind CSS | 4.x | токены из макета как CSS-переменные |
| TypeScript | 6.0 | не 7.0: typescript-eslint из eslint-config-next поддерживает только TS < 6.1 |
| ESLint | 9.39 | не 10: eslint-plugin-react, import и jsx-a11y не заявляют поддержку ESLint 10 |
| Vitest | 5.x | модульные тесты |
| next-intl | 4.x | переводы интерфейса, префиксы языков в URL |
| Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» |
| ALTCHA | 3.x | капча без третьих сторон |
| grammY | 1.x | Telegram-боты, этап 3 |
| Stripe SDK | актуальный | этап 4 |
Точные версии фиксируются в `package.json` на этапе 1a. Если к старту этапа 1a выйдет стабильный Payload 4, начинаем сразу на нём.
Точные версии закреплены в `package.json`: на этапе 1a — Next 16.4.2, Payload 3.91.0, React 19.3.1, next-intl 4.14.9, Tailwind 4.3.3, pnpm 12.14. На старте 1a Payload 4 был в бете (4.0.0-beta.3), поэтому начали на 3.91 ([D-002](decisions/D-002-payload-version.md)). Телеметрия Next.js и Payload выключена.
## 2. Одно приложение на все домены
1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)).
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит запись в коллекции «Домены» и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список доменов кэшируется в памяти и сбрасывается при изменении записи.
3. Proxy переписывает запрос во внутренний маршрут вида `/{тип}/{сайт}/{язык}/…`, для маркетинговых страниц ещё и `/{валюта}/`. Снаружи эти пути недоступны: прямой запрос к ним даёт 404.
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит домен и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список активных доменов proxy берёт у `/svc/internal/domains` (по токену из `.env`) и держит в памяти 15 секунд: правка домена в админке применяется в пределах 15 секунд ([D-022](decisions/D-022-proxy-routing.md)).
3. Proxy переписывает запрос во внутренний маршрут по хосту: `/m/{хост}/{язык}/{валюта}/…` — сайт, `/c/{хост}/{язык}/…` — кабинет, `/t/{хост}/…` — MiniApp. Снаружи эти пути недоступны: прямой запрос к ним получает ещё один префикс и попадает в 404. Решения proxy — чистая функция `src/core/routing/decide.ts` с модульными тестами.
4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты.
5. Сайтам, кабинетам и MiniApp хост админки не нужен:
a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос;
@@ -32,29 +35,38 @@
d. файлы медиатеки отдаются через домены сайтов.
6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному.
## 3. Структура кода (план)
## 3. Структура кода
```
src/
proxy.ts — домен → сайт, тип фронтенда, язык, валюта
proxy.ts — хост → домен, язык, валюта, внутренний маршрут
payload.config.ts
payload-types.ts — типы Payload (pnpm generate:types)
app/
(payload)/ — админка и REST API Payload (стандартные файлы)
(front)/m/[site]/[locale]/[currency]/… — маркетинговые сайты
(front)/c/[site]/[locale]/… — кабинеты и оформление заказа
(front)/t/[site]/… — MiniApp (этап 3)
svc/… — свои эндпоинты: ALTCHA, вебхуки, health
(front)/m/[host]/[locale]/[currency]/(site)/… — маркетинговые сайты
(front)/c/[host]/[locale]/(shell)/… — кабинет с навигацией
(front)/c/[host]/[locale]/(plain)/… — вход и оформление заказа
(front)/t/[host]/… — MiniApp (этап 3)
(front)/context.ts — домен, язык и валюта страницы, данные оболочек
svc/… — свои эндпоинты: health, internal/domains; дальше ALTCHA, вебхуки
payload/
collections/ globals/ hooks/ access/ fields/
collections/ access/ fields/ hooks/ components/
core/ — бизнес-логика без привязки к UI
billing/ keys/ auth/ referral/ email/ payments/ currency/ geo/
sites.ts domains/ routing/ currencies/ payments/ — этап 1a
billing/ keys/ auth/ referral/ email/ geo/ — следующие этапы
brands/
dedicated/ mass/ — страницы и компоненты каждого бренда
ui/ — общие компоненты: кнопки, поля, шиты, переключатели, флаги, карта
i18n/ — конфиг next-intl и messages/{en,ru,de,fr,es}.json
styles/ — Tailwind, токены брендов, темы
dedicated/ mass/ — различия брендов; страницы и компоненты — с этапа 1b
ui/ — общие компоненты: оболочки сайта и кабинета, переключатели, тема, логотип, иконки
i18n/ — языки, конфиг next-intl и messages/{en,ru,de,fr,es}.json
styles/ — Tailwind, токены брендов
seed/ — начальные данные (pnpm seed)
ops/systemd/ — юниты systemd
scripts/ — служебные скрипты (очистка dev-базы)
```
Корневые layout сайта и кабинета дают только документ: `<html>` с `lang`, `data-site` и скриптом темы. Шапка, подвал и навигация — во вложенных группах `(site)`, `(shell)` и `(plain)`.
Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (на этапе 3) бота.
## 4. Окружения
@@ -66,7 +78,7 @@ src/
| Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` |
| Порт | 3100 | 3000 |
| База | `veilix_dev` | `veilix_prod` |
| Схема базы | синхронизируется автоматически (push) | миграции Payload, применяются при выкладке |
| Схема базы | синхронизируется автоматически (push); если push задаёт вопросы — `pnpm db:reset` и `pnpm seed` | миграции Payload, применяются при выкладке |
| Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` |
| Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` |
| Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` |
@@ -77,7 +89,7 @@ src/
## 5. Выкладка в prod
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза.
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза. До первой выкладки (этап 1h) миграции не накапливаем: перед ней создаём одну начальную по итоговой схеме. Механизм проверен на этапе 1a: миграция, применённая к пустой схеме, создаёт те же таблицы, что push в dev.
2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 1h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги:
a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`;
b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`;
@@ -89,7 +101,7 @@ src/
## 6. Рендеринг и кэширование
1. Маркетинговые страницы собираются статически во всех вариантах «язык × валюта» и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
1. Маркетинговые страницы собираются статически в вариантах «домен × язык × валюта» при первом запросе (ISR) и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
2. Изменение в админке (цена, статус локации, FAQ) вызывает хук Payload, и Next.js пересобирает затронутые страницы за секунды (on-demand revalidation по тегам). Запасной вариант — плановое обновление раз в 5 минут.
3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет.
4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша.
+18 -9
View File
@@ -8,6 +8,8 @@
4. Деньги — целое число в минимальных единицах (`amount`: 249900 = 2 499,00 ₽) плюс `currency` — код ISO 4217 (`RUB`, `USD`). Код проверяется по справочнику «Валюты».
5. Даты — `timestamptz`, в UTC.
6. Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в `.env`).
7. Валюта везде хранится кодом ISO 4217 — и в деньгах, и в настройках (валюты домена и платёжной системы, строки цен). Код проверяется по справочнику, в админке это выпадающий список из «Валют».
8. Переводы полей Payload хранятся в таблицах `<коллекция>_l10n` (настройка адаптера `localesSuffix`). Имя по умолчанию `<коллекция>_locales` совпало бы с таблицей поля `locales` у сайтов и доменов.
## 2. Схема связей
@@ -48,6 +50,7 @@ erDiagram
| code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы |
| name | text (L) | «Вейликс» / «Veilix» |
| orderPrefix | text | префикс номеров: `VX`, `VV` |
| locales | multi-select из языков конфига | языки контента сайта; en обязателен; домены показывают часть этих языков ([D-023](decisions/D-023-site-locales.md)) |
| supportEmail | email | почта поддержки (Q-08) |
| emailFrom | group: name, address | отправитель писем, напр. «Вейликс» <no-reply@veilix.app> |
| supportBot, mainBot | text | имена ботов (этап 3) |
@@ -60,22 +63,26 @@ erDiagram
| expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» |
| seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию |
На этапе 1a сделаны `code` (после создания не меняется), `name`, `orderPrefix` и `locales`. Остальные поля добавляют этапы, которые ими пользуются: SEO и контакты — 1b, отправитель писем — 1c, сроки и пресеты — 1d и 1f, рефералы — 1g, боты — 3.
### 3.2 `domains` — Домены (сайт)
| Поле | Тип | Описание |
|---|---|---|
| hostname | text, уникальное | `veilix.app`, `my.veilix.app`, … |
| app | select `marketing` \| `cabinet` \| `miniapp` | тип фронтенда |
| locales | multi-select из языков конфига | включённые языки |
| defaultLocale | select | обычно `en` |
| locales | multi-select из языков конфига | включённые языки — часть языков сайта |
| defaultLocale | select | обычно `en`; открывается без префикса в адресе |
| currencies | multi-select валют | включённые валюты |
| defaultCurrency | select | валюта по умолчанию |
| localeCurrency | array: locale, currency | валюта по умолчанию для языка: ru → RUB |
| isPrimary | checkbox | основной домен этого типа; используется в ссылках и sitemap |
| isPrimary | checkbox | основной домен этого типа (у сайта один на каждый тип); используется в ссылках и sitemap |
| indexable | checkbox | разрешена ли индексация (для кабинета и MiniApp — нет) |
| umamiWebsiteId | text | этап 1h |
| active | checkbox | |
Язык и валюта по умолчанию должны входить в списки домена, язык в правилах «язык → валюта» не повторяется — это проверяется при сохранении.
### 3.3 `currencies` — Валюты
| Поле | Тип | Описание |
@@ -99,10 +106,10 @@ erDiagram
| icon | upload | |
| currencies | multi-select валют | в каких валютах доступна |
| purposes | multi-select `order` \| `topup` | оплата заказа, пополнение |
| visibleToCustomers | checkbox | у `manual` выключено: ей пользуются только администраторы |
| visibleToCustomers | checkbox | у `manual` всегда выключено (проверяется при сохранении): ей пользуются только администраторы |
| mode | select `test` \| `live` | |
| limits | array: currency, min, max | |
| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod` |
| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod`; объявлены в `src/core/payments/adapters.ts`, в базе не хранятся |
| active, sort | | |
Ключи доступа к провайдерам — только в `.env` (`PAYMENT_<КОД>_…`), не в базе.
@@ -123,7 +130,7 @@ erDiagram
| country | relationship → countries | уникальна в пределах сайта |
| city | text (L) | |
| lat, lng | number | точка города на карте |
| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро» |
| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро». Чужой для бренда статус не сохранится |
| freeServers | number, необязательное | счётчик (решение о показе — этап 1b) |
| visible, sort | | |
@@ -153,13 +160,13 @@ erDiagram
### 4.1 `admins` — Администраторы
Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`.
Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`. Первый администратор окружения (экран первого входа) становится owner. Последнего owner нельзя лишить роли и удалить. Роли и сайты меняет только owner, остальные правят только своё имя и пароль.
| Поле | Тип | Описание |
|---|---|---|
| name | text | |
| roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 |
| sites | доступ к тенантам | owner и admin видят все сайты |
| sites | доступ к тенантам | owner и admin видят все сайты, поэтому поле показывается только ролям support и content; на экране первого входа скрыто |
### 4.2 `customers` — Пользователи (сайт)
@@ -362,7 +369,9 @@ QR-коды строятся на лету из `payload`, не хранятся
| Коллекция | owner, admin | support | content | пользователь (через приложение) |
|---|---|---|---|---|
| Настройки и справочники | всё | чтение | чтение | — |
| Настройки и справочники | всё (удаление сайта — только owner) | чтение | чтение | — |
| admins | owner — всё, admin — только себя | только себя | только себя | — |
| media | всё | чтение | всё | файлы открыты всем: их показывают сайты |
| pages, faq, email-templates, локализация | всё | чтение | всё | — |
| customers | всё | чтение, правка заметок, блокировка | — | только свой профиль |
| keys, connections | всё | всё | — | только свои, чтение |
+1 -1
View File
@@ -87,7 +87,7 @@
1. Стандартная авторизация Payload: email и пароль, cookie только на хосте админки.
2. Админка открывается только на `plc.veilix.cc` и `dev.plc.veilix.cc`; nginx пускает туда только с разрешённых IP.
3. 5 неудачных попыток — блокировка на 15 минут.
4. Первый администратор каждого окружения создаётся на экране первого входа Payload.
4. Первый администратор каждого окружения создаётся на экране первого входа Payload и автоматически получает роль owner.
## 8. Cookies
+3 -3
View File
@@ -20,7 +20,7 @@
## 3. URL
1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`.
1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`. Точнее, без префикса открывается язык домена по умолчанию (`domains.defaultLocale`, сейчас везде en), а адрес с его префиксом (`/en/pricing`) перенаправляется на адрес без префикса.
2. Так же в кабинете: `my.veilix.app/keys`, `my.veilix.app/ru/keys`.
3. MiniApp: адрес в BotFather один, поэтому язык берётся из профиля пользователя или из Telegram; префикс в URL необязателен.
4. Пути одинаковые для всех языков (английские): `/pricing`, `/locations`, `/support`, `/legal/terms`, `/keys`, `/billing`, `/referral`, `/order`, `/login`.
@@ -29,9 +29,9 @@
## 4. Определение языка
1. Порядок: язык из URL → выбранный ранее (cookie `NEXT_LOCALE`, у вошедшего — язык профиля) → язык браузера (`Accept-Language`) среди включённых → страна из заголовка nginx `X-Geo-Country` (MaxMind) → `en`.
2. Перенаправление на версию с префиксом — только при первом заходе на адрес без префикса, без cookie, и только если выбран не английский. Поисковых ботов не перенаправляем никогда: так рекомендует Google.
2. Перенаправление на версию с префиксом — с адреса без префикса и только если выбран не язык домена по умолчанию ([D-024](decisions/D-024-locale-redirect.md)). Есть cookie `NEXT_LOCALE` — ведём на язык из cookie; cookie нет — определяем по языку браузера и стране. Перенаправляются только GET и HEAD. Поисковых ботов не перенаправляем никогда: так рекомендует Google.
3. Переключатель языка ставит cookie `NEXT_LOCALE`. У вошедшего пользователя язык сохраняется ещё и в профиле: на нём уходят письма и сообщения бота.
4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и страны Латинской Америки → es; остальные → en.
4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и испаноязычные страны Латинской Америки → es (Бразилия — en, пока нет pt-br); остальные → en. Таблица — `src/core/routing/locale.ts`.
## 5. SEO
+9 -1
View File
@@ -32,6 +32,7 @@
| `/order` | оформление заказа: шаги «Почта» и «Оплата», сводка |
| `/billing` | баланс, пополнение, карта, ближайшие списания, история |
| `/referral` | реферальная программа |
| `/more` | вкладка «Ещё» на узком экране: счёт, рефералы, поддержка, тема, язык |
| `/r/[код]` | реферальная ссылка: ставит cookie и ведёт на сайт бренда |
Навигация по макету: на широком экране — боковая панель, на узком — плавающая панель вкладок «Главная, Ключи, Купить, Ещё».
@@ -39,7 +40,7 @@
## 4. Темы и токены
1. Tailwind v4: токены макета — CSS-переменные, подключённые в `@theme`. Бренд задаёт атрибут `data-site="dedicated|mass"` на `<html>`, тему — `data-theme="light|dark"`. Режим «Как в системе» следует `prefers-color-scheme`.
2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage, тема применяется скриптом в `<head>` до отрисовки.
2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage (ключ `vx_theme`), тема применяется скриптом в `<head>` до отрисовки. Новый посетитель видит «Как в системе». localStorage у каждого хоста свой: тема, выбранная на сайте, не переходит в кабинет.
3. Токены в OKLCH, как в макете. Ниже — для ориентира; точные значения — в `<style>` прототипов, при расхождении верен прототип.
| Токен | Вейликс светлая | Вейликс тёмная | ВейликсВПН светлая | ВейликсВПН тёмная |
@@ -60,6 +61,8 @@
Общие для брендов: `--warn` (62% .14 62 / 78% .13 62), `--tg` (62% .15 250 / 70% .13 250), `--veil` (белый 55% / 7%), `--shadow`, цвета карты `--land`, `--land-line`, `--ocean`.
В коде токены — `src/styles/tokens.css`, утилиты Tailwind называются так же (`bg-surface`, `text-ink-2`, `bg-sel`); стандартная палитра Tailwind отключена. У Вейликса `--sel` равен `--accent-soft`. Цвета карты Вейликса в `Main` светлее, чем в `Locations`; на этапе 1a взяты из `Locations`, окончательно решаем на этапе 1b.
## 5. Шрифты, иконки, логотип
1. Commissioner 300–600 — весь текст; Unbounded 600 — только надпись логотипа. Подключение через `next/font`: файлы скачиваются при сборке и отдаются с нашего домена, без запросов к Google Fonts. Наборы символов: latin, latin-ext, cyrillic.
@@ -85,6 +88,11 @@
9. Плашки способов оплаты в подвале — по активным платёжным системам сайта.
10. Добавляем то, чего нет в макете: переключатель валюты, `<title>` и мета-описания, H1 на каждой странице, поддержку `prefers-reduced-motion` для анимаций.
11. Показывать ли на сайте количество свободных серверов (в MiniApp макет его показывает) — решаем на этапе 1b. По умолчанию — только статус.
12. Переключатели при 5 языках и с валютой (в макете только RU/EN, валюты нет). В шапке — выпадающие списки языка и валюты. В подвале, мобильном меню и кабинете — сегменты с кодами языков (EN RU DE FR ES) и символами валют (₽ $). В оболочке кабинета валюты нет: она появится в настройках (1f).
13. В подвале четыре документа: добавлена «Политика cookies». Плашек способов оплаты нет, пока нет видимых пользователю платёжных систем (пункт 9).
14. Кабинет mass: подписи навигации те же, что у dedicated («Список ключей», «Купить ключ», «Ключи»). Подписи макета MassApp («Подключения», «Выбрать страну», «Подписка») не используем (пункт 4).
15. Ширина надписи логотипа — по содержимому: в макете она подогнана под русское название.
16. 404. При `notFound()` Next 16 отдаёт пустую оболочку (`<html id="__next_error__">`), а страница 404 в оформлении бренда отрисовывается в браузере; статус ответа — 404. Серверную отрисовку 404 и 500 решаем на этапе 1b вместе с их оформлением: вариант — `global-not-found.js`.
## 8. Состояния, которых нет в макетах
+5 -4
View File
@@ -28,7 +28,7 @@ vx — личный пользователь администратора, к п
## 3. Процессы
1. Каждый сервис — под своим пользователем ОС, как systemd user-юнит. Root один раз включает `loginctl enable-linger` для vx_dev, vx_prod и umami, чтобы юниты работали без входа в систему.
2. `veilix-dev.service` (vx_dev): `pnpm dev` на порту 3100 из `/home/vx_dev/veilix_dev`, перезапуск при сбое.
2. `veilix-dev.service` (vx_dev): `pnpm dev` на 127.0.0.1:3100 из `/home/vx_dev/veilix_dev`, перезапуск при сбое. Файл юнита — `ops/systemd/veilix-dev.service` в репозитории, подключён через `systemctl --user link`.
3. `veilix.service` (vx_prod): `pnpm start` на порту 3000 из `/home/vx_prod/veilix/current`, `NODE_ENV=production`, перезапуск всегда.
4. `umami.service` (umami): Umami на порту 3300.
5. Фоновые задачи Payload выполняются внутри процессов приложения ([02-architecture.md](02-architecture.md), раздел 7).
@@ -89,12 +89,12 @@ vx — личный пользователь администратора, к п
| № | Этап | Задача | Статус на 21.10.2026 |
|---|---|---|---|
| R-00 | 0 | Переезд разработки: перенести `/home/vx/veilix_dev` в `/home/vx_dev/veilix_dev` (владелец vx_dev); SSH-ключ vx_dev для VSCode; Claude Code под vx_dev | не сделано |
| R-01 | 0 | Репозиторий в Gitea; SSH-ключ vx_dev с правом push; для vx_prod — ключ только на чтение (deploy key) | не сделано |
| R-00 | 0 | Переезд разработки: перенести `/home/vx/veilix_dev` в `/home/vx_dev/veilix_dev` (владелец vx_dev); SSH-ключ vx_dev для VSCode; Claude Code под vx_dev | сделано |
| R-01 | 0 | Репозиторий в Gitea; SSH-ключ vx_dev с правом push; для vx_prod — ключ только на чтение (deploy key) | не сделано: remote в репозитории нет |
| R-02 | 1a | PostgreSQL: роли `vx_dev`, `vx_prod`, `umami` и их базы (раздел 5) | сделано и проверено: vx_dev входит в `veilix_dev`, в `veilix_prod` — отказ |
| R-03 | 1a | pnpm: `corepack enable pnpm` | сделано |
| R-04 | 1a | Пользователи vx_dev, vx_prod, umami; `loginctl enable-linger` для них | сделано |
| R-05 | 1a | nginx и сертификаты для всех хостов (раздел 4) | работает; осталось исправить опечатку в `conf/ip_list_admin.conf` и добавить `X-Robots-Tag` для `dev.tg.*` |
| R-05 | 1a | nginx и сертификаты для всех хостов (раздел 4) | сделано: опечатка исправлена, `X-Robots-Tag` на всех dev-хостах |
| R-06 | 1a | MaxMind: `geoipupdate` (ключ в `/etc/GeoIP.conf`, база в `/var/lib/GeoIP`, таймер `geoipupdate.timer`); отключить старый модуль GeoIP v1; блок `geoip2` (раздел 4, пункт 4) | сделано |
| R-07 | 1c | Postfix, OpenDKIM, DNS-записи SPF, DKIM, DMARC — или данные SMTP-провайдера | |
| R-08 | 1h | PostgreSQL для prod — если не сделано вместе с R-02 | сделано в R-02 |
@@ -106,3 +106,4 @@ vx — личный пользователь администратора, к п
| R-14 | 4 | Аккаунты платёжных систем (сначала тестовые), адреса вебхуков | |
| R-15 | 1h | По желанию (Q-18): sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки | |
| R-16 | 1h | По желанию: правило nftables — к 127.0.0.1:3000 подключаются только `www-data` (nginx) и vx_prod ([10-security.md](10-security.md), раздел 7) | |
| R-17 | 1a | nginx dev: отдельный `location /_next/webpack-hmr` с `proxy_read_timeout 1h` в server-блоках dev-хостов и `dev.plc.veilix.cc`. Иначе WebSocket горячей перезагрузки закрывается каждые 30 секунд (`proxy_read_timeout 30` в `nginx.conf`) | |
+2
View File
@@ -17,6 +17,8 @@
2. Роли owner, admin, support, content ([03-data-model.md](03-data-model.md), раздел 10). Поле `debugCode` журнала писем видят только owner и admin.
3. Блокировка после 5 неудачных попыток входа.
4. Денежные операции, выдача и блокировка ключей, объединение аккаунтов пишутся в журнал действий. Журнал нельзя изменить.
5. GraphQL Payload выключен. Телеметрия Next.js и Payload выключена.
6. Служебный эндпоинт `/svc/internal/domains` отвечает только с токеном `INTERNAL_TOKEN` из `.env`, иначе 404. Cookie администратора — только по HTTPS.
## 4. Пользователи
+26
View File
@@ -0,0 +1,26 @@
# D-022. Proxy: внутренние маршруты по хосту, домены из внутреннего эндпоинта
Статус: принято, 21.10.2026. Уточняет [D-005](D-005-multitenancy.md), пункт 3.
## Контекст
1. Языки, валюты, основной домен и индексация заданы у домена, а не у сайта. Страница, собранная на весь сайт, не знает, на каком зеркале её открыли: у зеркала со своим набором языков переключатель, `hreflang` и `canonical` были бы чужими.
2. Proxy в Next 16 работает отдельно от кода страниц, и документация Next запрещает полагаться на общие с ним модули и глобальные переменные. Payload внутри proxy создал бы второй экземпляр поверх общего `global._payload`.
## Решение
1. Во внутреннем маршруте — хост: `/m/{хост}/{язык}/{валюта}/…` — сайт, `/c/{хост}/{язык}/…` — кабинет, `/t/{хост}/…` — MiniApp. Сайт и настройки страница берёт из записи домена.
2. Proxy получает список активных доменов у `/svc/internal/domains` (заголовок `x-vx-internal-token` с `INTERNAL_TOKEN` из `.env`) и держит его в памяти 15 секунд. Устаревший список отдаётся сразу и обновляется в фоне. Правка домена в админке применяется в пределах 15 секунд.
3. Proxy свой, а не middleware next-intl: тот не умеет брать домены из базы, класть хост и валюту в путь и учитывать `X-Geo-Country`. next-intl используется для сообщений, форматирования и ссылок. Ботов определяет библиотека `isbot`.
4. `skipProxyUrlNormalize: true` в `next.config.ts`. Без него адрес запроса в proxy строится от `localhost`, а роутер Next слушает `127.0.0.1`, и любое переписывание считается внешним.
## Рассмотренные варианты
1. Код сайта во внутреннем маршруте, как было в 02 — ломается, как только у зеркала свой набор языков или валют.
2. Local API Payload прямо в proxy — второй экземпляр Payload и пул соединений в отдельной сборке, конфликт по `global._payload`.
3. Сброс кэша proxy из хука Payload — хук срабатывает до фиксации транзакции, и proxy мог бы перечитать старые данные; при кэше в 15 секунд выигрыш не нужен.
## Последствия
1. Статических вариантов страниц столько, сколько сочетаний «домен × язык × валюта»; сейчас у сайта по одному домену каждого типа.
2. Если эндпоинт недоступен, proxy работает по последнему полученному списку, а без него отвечает 503.
+21
View File
@@ -0,0 +1,21 @@
# D-023. Языки сайта
Статус: принято, 21.10.2026. Уточняет [D-006](D-006-localization.md), пункты 4 и 5.
## Контекст
В модели данных языки были только у доменов. Но языки контента — свойство сайта: домен может показывать хоть один язык, а контент сайта нужен на всех его языках.
## Решение
1. У сайта поле `locales` — языки контента. По умолчанию все языки конфига. Английский обязателен: на него откатывается непереведённый контент.
2. Языки домена — часть языков сайта. Язык нельзя убрать у сайта, пока он включён на каком-то домене сайта.
3. `filterAvailableLocales` показывает в админке языки сайта, выбранного в переключателе; без выбранного сайта — все языки.
## Рассмотренные варианты
Языки сайта как объединение языков его доменов — без отдельного поля, но тогда контент на языке существует, только пока язык включён на домене.
## Последствия
Когда появятся pt-br и ja, их можно включить одному бренду: редакторы второго не увидят пустых языков.
+18
View File
@@ -0,0 +1,18 @@
# D-024. Перенаправление на выбранный язык
Статус: принято, 21.10.2026. Уточняет [D-007](D-007-language-detection.md), пункт 2.
## Контекст
По D-007 перенаправление шло только при первом заходе без cookie. Тогда выбравший русский при следующем заходе на `veilix.app/` видел бы английский.
## Решение
1. Без префикса открывается язык домена по умолчанию (`domains.defaultLocale`, сейчас везде en). Адрес с его префиксом (`/en/…`) перенаправляется на адрес без префикса.
2. Адрес без префикса и cookie `NEXT_LOCALE` с языком, включённым на домене, — перенаправление на этот язык, если он не язык по умолчанию.
3. Cookie нет — язык браузера среди включённых на домене, затем страна из `X-Geo-Country`, затем язык по умолчанию; перенаправление, если вышел не язык по умолчанию.
4. Cookie ставит только переключатель языка. Поисковых ботов (`isbot`) не перенаправляем никогда; перенаправляются только запросы GET и HEAD.
## Последствия
Так же по умолчанию работает next-intl. Выбор английского в переключателе тоже запоминается и отключает перенаправление по языку браузера.
+6 -3
View File
@@ -8,9 +8,9 @@
| [D-002](D-002-payload-version.md) | Payload 3, переход на 4 после стабильного релиза | принято |
| [D-003](D-003-hosting.md) | Без Docker: всё на хосте, процессы под systemd | принято |
| [D-004](D-004-environments.md) | Dev и prod на одном сервере под разными пользователями | принято |
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято |
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято |
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято |
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято, уточнено в D-022 |
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято, уточнено в D-023 |
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято, уточнено в D-024 |
| [D-008](D-008-uuid.md) | UUID и случайные публичные коды | принято |
| [D-009](D-009-billing-model.md) | Заказы, платежи, движения по счёту; всё через баланс | принято |
| [D-010](D-010-currencies.md) | Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам | принято |
@@ -25,3 +25,6 @@
| [D-019](D-019-external-services.md) | Chatwoot и Twenty CRM на отдельном VPS | принято |
| [D-020](D-020-key-models.md) | Модели ключей dedicated и mass | принято |
| [D-021](D-021-admin-host.md) | Админка на plc.veilix.cc с доступом по IP | принято |
| [D-022](D-022-proxy-routing.md) | Proxy: внутренние маршруты по хосту, домены из внутреннего эндпоинта | принято |
| [D-023](D-023-site-locales.md) | Языки сайта — поле `sites.locales`, домены показывают их часть | принято |
| [D-024](D-024-locale-redirect.md) | Перенаправление на выбранный язык | принято |
+21 -1
View File
@@ -1,6 +1,6 @@
# Этап 1a. Фундамент
Статус: не начат.
Статус: готово 21.10.2026. Остался R-17 (root, nginx), см. раздел 6.
## 1. Цель
@@ -37,3 +37,23 @@ Root: R-00…R-06 ([09-infrastructure.md](../09-infrastructure.md), раздел
5. В URL админки — UUID.
6. `dev.my.*` показывает оболочку кабинета; `/admin` на доменах брендов — 404.
7. `pnpm build`, линтер и проверка типов проходят.
## 6. Итоги
Проверено curl через nginx и напрямую на 127.0.0.1:3100: пункты 2, 3, 5, 6 и 7, а также HTTPS, `lang`, `data-site`, скрипт темы в `<head>` и токены обоих брендов в CSS. Внешний вид сайтов и кабинета и админку (пункты 1 и 4) проверил пользователь в браузере, первый администратор создан.
Решения и отклонения от плана:
1. Стек: TypeScript 6.0 и ESLint 9.39 вместо последних 7 и 10 — их пока не поддерживают плагины ([02-architecture.md](../02-architecture.md), раздел 1).
2. Внутренние маршруты — по хосту, а не по коду сайта; домены proxy берёт у `/svc/internal/domains` с кэшем на 15 секунд ([D-022](../decisions/D-022-proxy-routing.md)).
3. Языки сайта — поле `sites.locales` ([D-023](../decisions/D-023-site-locales.md)); перенаправление на язык из cookie ([D-024](../decisions/D-024-locale-redirect.md)).
4. В `sites` сделаны только `code`, `name`, `orderPrefix` и `locales`; остальные поля добавят этапы, которые ими пользуются ([03-data-model.md](../03-data-model.md), раздел 3.1).
5. Таблицы переводов Payload — с суффиксом `_l10n`; для вопросов push в dev — `pnpm db:reset`.
6. Миграции: механизм проверен на пустой схеме; начальную миграцию создаём перед первой выкладкой (1h).
7. Mass в USD — временные цены $6.99 и $69.90 до решения Q-02.
8. Адаптация макетов: переключатели для 5 языков и валюты, «Политика cookies» в подвале, подписи кабинета mass как у dedicated ([07-frontend.md](../07-frontend.md), раздел 7).
Осталось:
1. R-17 (root): `location /_next/webpack-hmr` с большим `proxy_read_timeout` в nginx dev — иначе горячая перезагрузка переподключается каждые 30 секунд.
2. На этап 1b: серверная отрисовка 404 и 500 ([07-frontend.md](../07-frontend.md), раздел 7.16), цвета карты, пересборка страниц по событию из Payload.