# Runtime-прогон `1.0.0-rc2` на чистом Debian 13 ```text Findings base: 82e5ca40 — дерево, на котором собран проверявшийся RC2 Fixes verified in: рабочее дерево этого прохода Хост: чистый Debian 13, установка с нуля из release-архива ``` Провенанс у этого файла другой, чем у соседних preflight-разборов: дефект наблюдался **на хосте**, а не найден чтением дерева. Установка прошла целиком и объявила успех, после чего панель оказалась недоступна. ## Статус прогона ```text RC2 BUILD ACCEPTANCE: PASS RC2 CLEAN INSTALL: PASS RC2 SERVER RUNTIME: PASS RC2 ADMIN LOGIN: FAIL RC2 OVERALL RUNTIME: REJECTED ``` Hysteria работает, сервер не повреждён, паника восстановима. Приёмка RC2 останавливается здесь: основная admin-панель после чистой установки недоступна целиком, и это P0 для release candidate. ## Сводка | ID | Дефект | Приоритет | Статус | | --- | --- | --- | --- | | LOGIN-01 | `POST /api/auth/login` паниковал на теге незарегистрированного правила | P0 | закрыт | | LOGIN-02 | Ни один тест не прогонял DTO через production-валидатор | P0 | закрыт | | LOGIN-03 | Установка объявляла успех, не проверив, что в панель можно войти | P0 | закрыт | | LOGIN-04 | Оркестратор не знал контракта учётных данных и по умолчанию писал невалидный логин | P0 | закрыт | | LOGIN-05 | Форма входа была строже сервера и запирала оператора после смены пароля | P1 | закрыт | | LOGIN-06 | Индикация ошибки на форме входа рисовалась вокруг не того элемента | P2 | закрыт | | LOGIN-07 | Класс символов записан диапазоном по опечатке в двух формах панели | P2 | закрыт | | LOGIN-08 | `last_login_at` объявлен в схеме, но не записывался никогда | P3 | закрыт | --- ## LOGIN-01 — вход паниковал до проверки учётных данных **Наблюдалось на хосте.** Каждый `POST /api/auth/login` отдавал HTTP 500. В journal на каждый запрос: ```text panic recovered: Undefined validation function 'validateStr' on field 'Username' controller/validator.go:115 controller/peer.go:50 ``` В `apps/model/dto/auth.go` на поле стоял тег `validateStr`: ```go Username *string `json:"username" ... validate:"required,min=6,max=32,validateStr"` ``` Правило с таким именем не регистрируется: при переименовании в `credentialStr` правка не доехала до одного файла. `go-playground/validator` на неизвестный тег ПАНИКУЕТ при разборе структуры — то есть до всякой проверки логина и пароля, — а `gin.Recovery` превращал панику в HTTP 500. Побочно это подтверждается тем, что зарегистрированное правило `credentialStr` не использовалось **нигде**: переименование оставило после себя мёртвую регистрацию и живую ссылку на несуществующее имя. **Закрыто.** Контракт учётных данных вынесен в `apps/service/admin_credentials.go` — по образцу уже существующего `IsValidPeerName`. Правило `credentialStr` зовёт его, длина живёт ВНУТРИ правила (два правила длины на одном поле уже приводили к необъяснимому отказу на имени пира), а обработчик входа переехал в `apps/controller/auth.go`: пока он лежал в `peer.go`, стек указывал на управление пирами — подсистему, не имеющую к отказу отношения. --- ## LOGIN-02 — 311 Go-тестов не видели дефекта **Наблюдалось по дереву.** `validator_test.go` проверял регексп `credentialStr` НАПРЯМУЮ, в обход валидатора, а обработчика входа не касался ни один тест. Важно, почему очевидная проверка не помогла бы. Прогон нулевого `LoginDto` через `validate.Struct` дефекта НЕ ловит: цепочка правил поля обрывается на первом несработавшем, поэтому на пустом `Username` проверка отказывает по `required` и до испорченного тега не доходит. **Закрыто** барьером, закрывающим КЛАСС, а не найденный экземпляр: `TestEveryValidationTagIsRegistered` извлекает все теги `validate:"…"` из `apps/model/**` и предъявляет каждый валидатору отдельно. Незарегистрированное правило паникует так же, как паниковало в бою, — но на сборке. Барьер проверен возвратом исходного тега: тест падает с именем правила и файлом. Сверх него добавлены прогон `LoginDto` через production-валидатор, таблица негативных случаев с ожидаемыми кодами причин и HTTP-регрессия обработчика — в том числе за `gin.Recovery`, то есть ровно в той конфигурации, в которой дефект наблюдался. --- ## LOGIN-03 — установка не проверяла, что в панель можно войти **Наблюдалось на хосте.** Установка завершилась `INSTALL EXIT CODE: 0` при полностью недоступной панели. Smoke отвечал на вопрос «работает ли панель» тремя фактами: юнит активен, `127.0.0.1:8080` в `LISTEN`, `/healthz` отвечает `ok: true`. Все три были истинны. Факт `LISTEN` не означает, что панель функциональна, — RC2 это буквально доказал. **Закрыто.** `orchestrator/src/steps/smoke.ts` выполняет настоящий `POST /api/auth/login` bootstrap-учётными данными и требует `code: 20000` с непустым `accessToken`; успех определяется КОНВЕРТОМ, а не кодом HTTP — админка отвечает `200 OK` и на отказ тоже. Отдельная отрицательная проба выполняется в любом режиме операции и не зависит от актуальности пароля: заведомо неверные учётные данные обязаны получить конверт отказа, а не 500. Учётные данные не попадают ни в текст ошибки, ни в журнал. --- ## LOGIN-04 — оркестратор не знал контракта, который сам порождает **Найдено при разборе смежного кода.** `orchestrator/src/config/env.ts` брал логин как ```ts adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin") ``` `admin` — пять символов при минимуме панели в шесть. При пустом `HY2XS_ADMIN_USER` установка проходила целиком и создавала учётную запись, под которой невозможно войти. Про одно и то же имя существовало три расходящихся умолчания: `admin` здесь, `hy2xsadmin` в `apps/dao/sqlite.go` и `hy2xsadmin` в `package/config/hy2xs.env`. Ни логин, ни операторский `HY2XS_ADMIN_INITIAL_PASSWORD` не проверялись против контракта панели вовсе. **Закрыто.** Оба значения проверяются при разборе окружения — той стороной, которая их ПОРОЖДАЕТ: отказ, пришедший установщику, чинится одной строкой в `hy2xs.env`, а неработающий вход на готовом сервере — переустановкой. Умолчание сведено к `hy2xsadmin` во всех трёх местах. Проверяется и сгенерированный пароль: генератор — такой же источник значения. --- ## LOGIN-05 — панель была строже сервера и запирала после смены пароля **Найдено при разборе панели.** Границы пароля различались на трёх сторонах: | Где | Логин | Пароль | | --- | --- | --- | | форма входа | 6-32 + набор | 6-**32** + набор | | форма смены пароля | — | 6-**64** + набор | | сервер | 6-32 + набор | 6-**64**, набора нет | Следствий два, и оба закрывают панель. Пароль длиннее 32 символов назначался штатной формой смены и после этого не вводился на форме входа: оператор терял доступ после операции, которую панель ему же и предложила. А набор символов на пароле отвергал значение, которое сервер принял бы, — в том числе `HY2XS_ADMIN_INITIAL_PASSWORD`, заданный оператором со знаком вне набора. **Закрыто.** Правило объявлено один раз в `apps/frontend/src/constants/credentials.ts` и используется обеими формами; набор символов с пароля снят — сервер его не предъявляет нигде, а проверка, умеющая только запереть оператора, не защищает ничего. Совпадение с Go-контрактом удерживается тестом, читающим Go-исходник. --- ## LOGIN-06 — красная рамка обводила не то, что видит оператор **Наблюдалось на экране.** При отказе проверки красная рамка ложилась вокруг одного лишь поля ввода: у логина начиналась после иконки пользователя, у пароля обрывалась перед переключателем видимости, и ни одна её сторона не совпадала с видимой границей поля. Причина — специфичность, а не опечатка. Element Plus 2.14.5 (`theme-chalk/src/form-item.scss`) рисует состояние отказа на `el-input__wrapper` селектором из ЧЕТЫРЁХ классов: ```text .el-form-item.is-error .el-form-item__content .el-input__wrapper ``` Форма входа рисует видимую рамку поля на `el-form-item` — внутрь одного поля кладутся иконка, ввод и переключатель видимости, — а штатную тень враппера гасила селектором из трёх классов и проигрывала. **Закрыто.** Индикация перенесена на элемент, который оператор и видит полем (`.el-form-item.is-error`), а тень враппера гасится селектором, повторяющим чужой и добавляющим атрибут scoped-стиля, — то есть выигрывает конкретностью, а не `!important`. Сообщению об отказе оставлено место под полем: `el-form-item__error` позиционируется абсолютно от `top: 100%` и живёт вне рамки. Проверены остальные формы панели: собственная рамка на `el-form-item` вместе с переопределением `el-input__wrapper` встречается только на форме входа. Смена пароля, диалог пира и тулбары используют штатную рамку, где `is-error` попадает точно. **Что машина не докажет.** Совпадение рамки с границей поля на экране остаётся ручной проверкой; тест закрепляет только наличие правил, которые её обеспечивают. --- ## LOGIN-07 — класс символов записан диапазоном по опечатке **Найдено при разборе панели.** В формах входа и смены пароля стояло ```text /^[a-zA-Z0-9!@#$%^&*()_+-=]{6,32}$/ ``` Дефис внутри класса не экранирован, поэтому `+-=` образует ДИАПАЗОН и впускает `, - . / 0-9 : ; < =`. С серверным набором это совпадало по совпадению: оба несли одну и ту же опечатку. В форме пира тот же класс уже был записан явно (`_+\-=`) — договорённость в проекте существовала и до входа не доехала. **Закрыто.** Класс записан явно и НЕ сужен: фактическое множество уже действует на установленных серверах. Экранирование дефиса закреплено тестом — пока набор выглядел опечаткой, любая попытка «навести порядок» развела бы панель и сервер обратно. --- ## LOGIN-08 — время последнего входа не записывалось никогда **Найдено при разборе.** Колонка `last_login_at` объявлена в схеме и в entity, `service.UpdateAdminLastLoginAt` существовал — и не вызывался ниоткуда. **Закрыто.** Отметка ставится в `service.Login`, сразу после успешной проверки пароля: это единственная дверь, и записать её оттуда невозможно забыть. Отказ записи вход НЕ отменяет — учётные данные уже подтверждены, — но пишется в журнал уровнем error: неписаная отметка есть расхождение между тем, что показывает панель, и тем, что произошло. --- ## Отдельно: диагностика на хосте `systemctl cat` открывает `less`, из-за чего вставленный следом блок перемешивается с pager. Для воспроизводимых прогонов используется ```bash SYSTEMD_PAGER=cat systemctl cat hy2xs-admin.service ``` или `systemctl --no-pager cat hy2xs-admin.service`. Юнит `hy2xs-admin.service` проверен и к дефекту отношения не имеет. ## Что делать с установленным RC2 На хосте ничего чинить вручную не нужно и не следует: hotpatch бинарника на проде и перезалив содержимого уже опубликованного `v1.0.0-rc2` противоречат воспроизводимости и immutable provenance, вокруг которых построен продукт. Правильный путь — исправленный source и новая сборка, а хост переустанавливается с нуля, чтобы проверка шла по тому же clean-host сценарию, а не поверх установленного RC2.