fix(admin): закрыть обещания панели, которые продукт не выполнял

Девятый проход, по итогам приёмки v1.0.0-rc1 на живом Debian 13. Общая тема:
интерфейс обещал оператору то, что продукт умел, но до чего не доходило
управление.

Секрет пира. Подпись под полем предлагала оставить его пустым, сервер умел его
сгенерировать, и генерация была недостижима: в go-playground/validator тег
omitempty НЕ пропускает правило, если поле объявлено указателем и указатель не
nil — hasValue считает указатель на пустую строку «значением». Правило min=6
применялось к пустой строке и отказывало. Ловушка закрыта общим шагом
нормализации DTO, а не тегом на одном поле: та же ловушка ломала фильтр списка
пиров, где очищенный крестиком el-input отправляет `?name=`. Граница проходит по
каждому полю отдельно — у remark пустая строка означает «убрать пометку», у
disabled ноль означает «включён».

Отказы. Любая ошибка любого поля превращалась в слово `invalid`, а слой vo
определял код ответа СРАВНЕНИЕМ текста сообщения — тот же антипаттерн, который
запрещён панели, только на сервере. Ответ несёт errors[{code, field, message,
params}]; панель выбирает фразу по коду и подставляет причины под поля.

Сессия. Ветка «войдите заново» была недостижима дважды: сервер отвечает HTTP 200
на любой отказ, поэтому обработчик ошибок axios не вызывался, а условие в нём
проверяло code === "A0230" и поле msg, которых в этом API никогда не было.
Истёкший токен вдобавок уезжал с кодом системной ошибки.

Иконки. Контракт currentColor был объявлен в двух местах и не действовал: восемь
ассетов несли литеральный fill="#000000" на <path>, а атрибут представления
перебивает унаследованное CSS-свойство. Под это попадали все семь иконок
бокового меню на фоне #181818.

Имя пира. Два правила на одном поле противоречили друг другу (min=1 против
6-32), а копия набора символов в слое контроллеров несла неэкранированный дефис
и впускала `, - . / : ; <` — через панель проходило имя peer/name, которое
импорт того же пира отклонял. Набор символов ЛОГИНА сознательно не сужен и
закреплён тестом: он приходит из HY2XS_ADMIN_USER и оркестратором не
ограничивается.

Добавлены подпись «Разработано во Flamy» с адресом, принадлежащим приложению, и
контрактные тесты панели как обязательный шаг сборки. Их исполняет Bun, а не
vitest: jsdom не вычисляет currentColor и визуальной корректности не доказал бы,
зато vitest привёл бы в граф pnpm audit сотню транзитивных зависимостей.

docs/ разложена по слоям, 11-testing-and-acceptance.md (117 КБ) разбит на пять
частей, добавлен docs/acceptance/ с отчётом о прогоне rc1 и перечнем дефектов.
Обход документации в приёмке стал рекурсивным: плоский docs/*.md после
разнесения по каталогам совпадал бы ровно с одним файлом.
This commit is contained in:
2026-09-01 07:27:15 +05:00
parent a1f0db22c2
commit c0a43ae915
86 changed files with 6237 additions and 1819 deletions
+164
View File
@@ -0,0 +1,164 @@
# Runtime env и post-install snapshot
## Цель документа
Зафиксировать двухслойную модель:
- editable runtime env: `/etc/hy2xs/hy2xs.env`
- generated deploy snapshot: `/etc/hysteria/post-install.env`
## Зачем нужен файл
После первичной установки оператору нужны:
1) runtime-файл, который оркестратор читает и валидирует;
2) snapshot-файл фактического deploy-состояния.
В snapshot видно:
- какой пакет был установлен
- какой build артефакт использован
- какой стек оркестратора применён
- какая версия Hysteria реально установилась
- какой build HY2XS admin разложен на target
- какие базовые параметры сети и портов заданы
Именно для этого создаётся `post-install.env`.
## Чего файл не делает
Этот файл:
- не делает оркестратор update-manager'ом
- не гарантирует автоматическое применение изменений
- не заменяет runtime-конфиги
- не превращает target в builder
## Рекомендуемые пути
```bash
/etc/hy2xs/hy2xs.env
/etc/hysteria/post-install.env
```
`/etc/hy2xs/hy2xs.env` и `/etc/hysteria/post-install.env` должны иметь права `0600 root:root`.
`/etc/hysteria/config.yaml` должен иметь права `0640 hysteria:hy2xs-admin` (UI только читает).
## Минимальный набор переменных
### Deploy / package
- `DEPLOY_TARGET_OS`
- `DEPLOY_TIMESTAMP` (last apply timestamp)
- `PACKAGE_NAME`
- `PACKAGE_BUILD_ID`
- `PACKAGE_VERSION`
### Orchestrator
- `ORCH_SOURCE_STACK=bun-typescript`
- `ORCH_BUILD_MODE`
- `ORCH_BUILD_ID`
- `ORCH_ENTRYPOINT`
### Общие
- `HY2XS_CONFIG_SCHEMA_VERSION`
- `DEPLOY_DOMAIN`
- `PUBLIC_HOST`
- `PUBLIC_PORT`
- `SSH_PORT`
- `HY2XS_FIREWALL_MODE`
- `HY2XS_FIREWALL_STAGED_APPLY`
- `HY2XS_ADMIN_USER`
- `HY2XS_FORCE_PASSWORD_CHANGE`
- `HY2XS_ALLOW_SELF_SIGNED_DEV`
### Hysteria
- `HY2_SOURCE=official-upstream`
- `HY2_VERSION` — фактически установленная версия
- `HY2_RESOLUTION` — как версия была выбрана при сборке: `latest-stable`, `pinned` или `override`
- `HY2_TLS_MODE`
- `HY2_ACME_EMAIL`
- `HY2_TLS_CERT_PATH`
- `HY2_TLS_KEY_PATH`
- `HY2_LISTEN_HOST`
- `HY2_PORT`
- `HY2_AUTH_MODE`
- `HY2_AUTH_URL`
- `HY2_TRAFFIC_STATS_LISTEN`
- `HY2_OBFS_TYPE``gecko` или `salamander`
- `HY2_OBFS_PASSWORD`
- `HY2_GECKO_MIN_PACKET_SIZE`
- `HY2_GECKO_MAX_PACKET_SIZE`
- `HY2_BANDWIDTH_UP`
- `HY2_BANDWIDTH_DOWN`
- `HY2_DISABLE_LOSS_COMPENSATION`
- `HY2_IGNORE_CLIENT_BANDWIDTH`
- `HY2_CONGESTION_TYPE`
- `HY2_BBR_PROFILE`
- `HY2_DISABLE_STATELESS_RESET`
- `HY2_CONFIG_PATH`
### HY2XS admin
- `HY2XS_ADMIN_ENABLED`
- `HY2XS_ADMIN_SOURCE`
- `HY2XS_ADMIN_BUILD_ID`
- `HY2XS_ADMIN_BIND_HOST`
- `HY2XS_ADMIN_PORT`
- `HY2XS_ADMIN_INSTALL_DIR`
- `HY2XS_ADMIN_DATA_DIR`
- `HY2XS_ADMIN_LOG_DIR`
## Как работать с файлами
Правильная модель:
1. оркестратор создаёт `hy2xs.env` и `post-install.env` при установке;
2. оператор редактирует только `hy2xs.env`;
3. оператор запускает `reconfigure --dry-run`, затем `reconfigure --apply`;
4. оркестратор обновляет runtime и перезаписывает snapshot.
### Политики проверок DNS
В `/etc/hy2xs/hy2xs.env` есть две независимые политики, обе по умолчанию
`strict`:
| Переменная | Что проверяет |
| --- | --- |
| `HY2XS_DNS_AAAA_POLICY` | наличие AAAA-записи при IPv4-only профиле |
| `HY2XS_PUBLIC_ENDPOINT_POLICY` | что A-записи публичного endpoint ведут на публичные IPv4 этого сервера |
`HY2XS_PUBLIC_ENDPOINT_POLICY` принимает `strict` / `warn` / `off`. Ослаблять
её имеет смысл только для топологий вне baseline: сервер за NAT, floating IP,
anycast. Отсутствие A-записи фатально при любом значении.
Обе политики применяются в `install`, `reconfigure` и `doctor`, потому что
живут в общем `preflight`.
### Сетевая идентичность админки
`HY2XS_UI_PORT`, `HY2XS_UI_BIND_HOST`, `HY2XS_DATA_DIR` и `HY2XS_LOG_DIR`
единственный источник истины для этих величин. Админка читает их из окружения
юнита и не хранит собственных копий в SQLite.
Важно:
- `HY2XS_ADMIN_INITIAL_PASSWORD` используется только для первичного bootstrap seed;
- `HY2XS_ADMIN_CON_PASS` — отдельная runtime-сущность для Hysteria auth/smoke;
- bootstrap secret хранится в явном формате `KEY=VALUE` (`ADMIN_USER`, `ADMIN_INITIAL_PASSWORD`, `ADMIN_CON_PASS`), права `0600`;
- `HY2XS_FORCE_PASSWORD_CHANGE` в production baseline установлен в `false` (forced UX-flow пока не реализован);
- после первичного seed перезапуски `hy2xs-admin` не должны переопределять пароль admin и `con_pass`.
### Immutable-bootstrap контракт
- `/etc/hy2xs/bootstrap-admin.secret` создаётся оркестратором только при первичной установке.
- На `reconfigure --apply` bootstrap secret не пересоздаётся и не ротируется автоматически.
- Изменения `HY2XS_ADMIN_INITIAL_PASSWORD` в runtime env после первичной установки не должны менять фактический пароль admin.
- `HY2XS_ADMIN_CON_PASS` используется как bootstrap-значение при первичной установке; после создания admin account изменение этого значения в `/etc/hy2xs/hy2xs.env` не пересоздаёт и не обновляет существующий `con_pass` в SQLite.
- Ротация `con_pass` выполняется через account-management слой UI/БД, а bootstrap snapshot остаётся неизменным.
## Что нельзя делать
- сваливать туда временный мусор
- считать, что edit env автоматически меняет runtime без `reconfigure --apply`
- использовать файл как замену настоящей конфигурации сервисов
## Пример
Используйте canonical runtime-файл `package/config/hy2xs.env` как базовый шаблон значений
и переносите его параметры в `/etc/hy2xs/hy2xs.env`.