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:
+153
-4
@@ -46,6 +46,155 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
результат на достаточно большом входе, опаснее отсутствующей: отсутствующая
|
||||
ничего не обещает.
|
||||
|
||||
Девятый проход — работа оператора в панели, по итогам приёмки `v1.0.0-rc1` на
|
||||
живом Debian 13. Общая тема прохода: обещания интерфейса, которые продукт не
|
||||
выполнял, хотя умел. Подпись под полем предлагала оставить секрет пустым, и
|
||||
сервер действительно умел его сгенерировать — до этой генерации не доходило
|
||||
управление. Контракт `currentColor` был объявлен в двух местах — и не
|
||||
действовал, потому что цвет был вписан в сами ассеты. Ветка «сессия истекла,
|
||||
войдите заново» существовала — и была недостижима сразу по двум причинам.
|
||||
|
||||
### Исправлено — панель оператора
|
||||
|
||||
- **Необязательный секрет пира был фактически обязателен.** Панель обещала
|
||||
«оставьте пустым — сгенерируем автоматически» и отправляла `secret: ""`.
|
||||
В `go-playground/validator` тег `omitempty` НЕ пропускает правило, если поле
|
||||
объявлено указателем и указатель не nil: помощник `hasValue` считает
|
||||
указатель на пустую строку «значением». Правило `min=6` применялось к пустой
|
||||
строке и отказывало, а генерация в `CreatePeer` оставалась недостижимой.
|
||||
|
||||
Ловушка закрыта механизмом, а не тегом на одном поле: между разбором тела и
|
||||
проверкой правил появился шаг нормализации DTO (`dto.Normalizable`). Граница
|
||||
проходит по каждому полю отдельно — у `remark` пустая строка означает
|
||||
«убрать пометку», у `disabled` ноль означает «включён», и общее правило
|
||||
«пусто → не задано» молча сломало бы оба.
|
||||
|
||||
Той же ловушкой ломался фильтр списка пиров: `el-input` с крестиком очистки
|
||||
ставит пустую строку, axios сериализует её как `?name=`, и поиск отказывал в
|
||||
один клик по крестику.
|
||||
|
||||
- **Генерация секрета названа явным шагом сервисного слоя.**
|
||||
`service.GeneratePeerSecret` на базе `util.RandomString` (`crypto/rand` с
|
||||
отбрасыванием смещённых байтов) используется и формой, и импортом: пир,
|
||||
созданный панелью, и пир, импортированный без секрета, теперь неотличимы.
|
||||
|
||||
- **Любая ошибка любого поля превращалась в слово `invalid`.** Слой `vo` при
|
||||
этом определял код ответа СРАВНЕНИЕМ текста сообщения с тремя литералами —
|
||||
тот же антипаттерн, который запрещён панели, только на сервере. Ответ об
|
||||
ошибке теперь несёт `errors: [{code, field, message, params}]`; панель
|
||||
выбирает локализованную фразу по коду и подставляет причины под поля формы.
|
||||
Границы числа и границы длины строки различаются кодом, хотя тег валидатора
|
||||
у них один: оператору это разные фразы.
|
||||
|
||||
- **Истечение сессии не обрабатывалось.** Сервер отвечает HTTP 200 на любой
|
||||
отказ, поэтому обработчик ошибок axios для отказов API не вызывался вовсе —
|
||||
а ветка сессии жила именно там; её условие проверяло `code === "A0230"` и
|
||||
поле `msg`, которых в этом API никогда не было. Вдобавок истёкший токен уезжал
|
||||
с кодом системной ошибки. Теперь `ParseToken` возвращает объявленные значения
|
||||
ошибок вместо свежих строк, middleware различает истечение и
|
||||
недействительность через `errors.Is`, а панель показывает диалог и
|
||||
возвращает на форму входа — один раз, даже когда истёкший токен уронил
|
||||
несколько параллельных запросов страницы.
|
||||
|
||||
- **Обработчик транспортных ошибок падал сам.** Он читал `error.response.data`,
|
||||
не проверив `error.response`, и при обрыве соединения подменял настоящую
|
||||
причину `TypeError` внутри себя.
|
||||
|
||||
- **Сброс сессии больше не зовёт `localStorage.clear()`**, который заодно стирал
|
||||
выбранный оператором язык панели.
|
||||
|
||||
- **`id` требовался и в пути, и в теле запроса.** `PeerUpdateDto` встраивал
|
||||
`IdDto` с правилом `required`, хотя значение из тела всё равно затирается
|
||||
значением из пути. Заодно убрана недостижимая запасная ветка `resolveID`,
|
||||
читавшая идентификатор из тела: она вызывала разбор тела, которое обработчик
|
||||
читает следом второй раз, а gin его не буферизует.
|
||||
|
||||
### Исправлено — отрисовка иконок
|
||||
|
||||
- **Контракт `currentColor` был объявлен и не действовал.** `fill: currentcolor`
|
||||
стоял и в `SvgIcon/index.vue`, и в `styles/sidebar.scss`, но восемь из
|
||||
семнадцати ассетов несли литеральный `fill="#000000"` прямо на `<path>`, а
|
||||
атрибут представления перебивает унаследованное CSS-свойство. Под это
|
||||
попадали все семь иконок бокового меню на фоне `#181818`.
|
||||
|
||||
Литеральный цвет убран из ассетов; многоцветные объявлены явным списком;
|
||||
преобразование в `<symbol>` и контракт ассета вынесены в чистый модуль
|
||||
`SvgIcon/symbol.ts`, который можно выполнить вне Vite и DOM — и, значит,
|
||||
проверить. Цвета в рантайме НЕ переписываются: молчаливая нормализация
|
||||
скрывала бы ровно тот дефект, который контракт обязан делать видимым.
|
||||
|
||||
- **У `SvgIcon` убран проп цвета** и атрибут `fill` на `<use>`: он приглашал
|
||||
чинить отрисовку точечно в обход общего контракта.
|
||||
|
||||
### Исправлено — правила имени пира
|
||||
|
||||
- **Два правила на одном поле противоречили друг другу.** Стояли
|
||||
`min=1,max=32` и `validateStr`, требовавший 6-32 символа: имя из трёх
|
||||
символов проходило одно правило и отказывалось на другом. Длина перенесена
|
||||
внутрь одного правила.
|
||||
|
||||
- **Набор символов в слое контроллеров впускал `, - . / : ; <`.** Копия правила
|
||||
несла неэкранированный дефис, из-за чего `+-=` образовывал ДИАПАЗОН; её
|
||||
комментарий при этом утверждал, что набор тот же, что у импорта. Через панель
|
||||
проходило имя `peer/name`, которое импорт того же пира отклонял, — при том что
|
||||
имя уезжает во fragment клиентской ссылки и в автогенерируемый секрет.
|
||||
Правило объявлено один раз (`service.IsValidPeerName`) и используется обеими
|
||||
дверями в таблицу пиров.
|
||||
|
||||
Набор символов ЛОГИНА администратора сознательно не сужен: он записан явно,
|
||||
но повторяет прежнее фактическое множество. Имя администратора приходит из
|
||||
`HY2XS_ADMIN_USER`, оркестратор его не ограничивает, и сужение правила
|
||||
означало бы, что установка с логином вроде `admin.ops` перестаёт пускать
|
||||
оператора в панель. Закреплено отдельным тестом, чтобы попытка «навести
|
||||
порядок» роняла сборку, а не вход на живом сервере.
|
||||
|
||||
### Добавлено — атрибуция и контрактные тесты панели
|
||||
|
||||
- **Подпись «Разработано во Flamy»** внизу бокового меню, ссылкой фирменным
|
||||
цветом. Адрес объявлен один раз в `apps/frontend/src/constants/branding.ts` и
|
||||
принадлежит приложению: он не читается ни из `hy2xs.env`, ни из config API,
|
||||
ни из таблицы `config`. Высота области прокрутки меню вычитает высоту
|
||||
подписи, поэтому пункты меню не могут на неё наехать.
|
||||
|
||||
- **Контрактные тесты панели** (`tools/test/frontend-*.test.ts`) стали
|
||||
обязательным шагом сборки наравне с тестами оркестратора и админки: контракт
|
||||
спрайта иконок, совпадение наборов ключей `ru` и `en`, соответствие кодов
|
||||
ошибок серверным константам, единственность адреса атрибуции.
|
||||
|
||||
Их исполняет уже закреплённый в `versions.env` Bun, а не vitest: jsdom не
|
||||
вычисляет `currentColor` и визуальной корректности всё равно не доказал бы,
|
||||
зато vitest привёл бы в граф `pnpm audit` — а его порог считается по всему
|
||||
lock-файлу frontend — сотню транзитивных зависимостей ради нулевой
|
||||
дополнительной гарантии.
|
||||
|
||||
### Изменено — документация
|
||||
|
||||
- **`docs/` разложена по слоям** вместо плоской кучи из четырнадцати файлов:
|
||||
`architecture/`, `build/`, `runtime/`, `admin/`, `operations/`, `testing/`,
|
||||
`acceptance/`. Двузначный префикс сохранён как стабильный идентификатор
|
||||
документа — под ним на него ссылаются CHANGELOG, релизные гейты и сообщения
|
||||
оркестратора.
|
||||
|
||||
- **`11-testing-and-acceptance.md` (117 КБ, 57 разделов) разбит на пять частей**
|
||||
по слоям, на которых выполняются проверки.
|
||||
|
||||
- **Добавлен `docs/acceptance/`** — отчёты о фактических прогонах приёмки,
|
||||
отдельно от описания самих проверок. Документ проверок переживает релизы;
|
||||
отчёт о прогоне относится к одному артефакту и одному хосту и после
|
||||
публикации не редактируется. Первый отчёт — build/host acceptance
|
||||
`v1.0.0-rc1` на Debian 13 с перечнем найденных дефектов и их закрытия.
|
||||
|
||||
- **Добавлен `docs/admin/15-ui-contracts.md`** — контракты панели, которые не
|
||||
проверяются ни типами, ни сборкой bundle.
|
||||
|
||||
- **Зафиксировано требование к памяти build-хоста:** `govulncheck` строит граф
|
||||
достижимости по всему модулю вместе со stdlib, и на машине с ~1.9 GiB RAM без
|
||||
swap он был убит OOM killer.
|
||||
|
||||
- **Обход документации в приёмке стал рекурсивным.** Плоский шаблон
|
||||
`docs/*.md` после разнесения по каталогам совпадал бы ровно с одним файлом,
|
||||
то есть проверка отчитывалась бы зелёным, не заглянув почти никуда.
|
||||
|
||||
### Исправлено — гейты сборки
|
||||
|
||||
- **Пайплайн в поиск с флагом `-q` под `pipefail` инвертирует смысл проверки.**
|
||||
@@ -1069,7 +1218,7 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
фрагмент nftables, systemd-юниты, база админки и наследие `0.x`. Пути
|
||||
установки и данных берутся из конфигурации, а не захардкожены.
|
||||
|
||||
- **`tools/legacy/purge-v0.sh`** и [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md) —
|
||||
- **`tools/legacy/purge-v0.sh`** и [docs/operations/14-legacy-cleanup.md](docs/operations/14-legacy-cleanup.md) —
|
||||
явная очистка сервера от предыдущего поколения. По умолчанию скрипт
|
||||
показывает план и ничего не делает; выполнение требует
|
||||
`--apply --yes-i-know`. Из установщика он не вызывается никогда: это вернуло
|
||||
@@ -1143,7 +1292,7 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
- **База админки — `hy2xs-admin.db`** вместо `h_ui.db`; reference-схема —
|
||||
`apps/docs/sql/schema.sql` вместо `h_ui_db.sql`. Совместимость сохранять не
|
||||
требуется: v1 ставится только с нуля. Историческое имя `h_ui.db` остаётся в
|
||||
[docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md) — там это имя чужого
|
||||
[docs/operations/14-legacy-cleanup.md](docs/operations/14-legacy-cleanup.md) — там это имя чужого
|
||||
артефакта, который очистка должна найти.
|
||||
|
||||
- **Индикатор загрузки и legacy-цвета переведены на брендовый токен.**
|
||||
@@ -1210,7 +1359,7 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
существует. Номера оставшихся миграций сохранены: перенумерация заставила бы
|
||||
их примениться повторно.
|
||||
|
||||
В `docs/14-legacy-cleanup.md` имена предыдущего поколения остаются — там они
|
||||
В `docs/operations/14-legacy-cleanup.md` имена предыдущего поколения остаются — там они
|
||||
обозначают реальные объекты, которые нужно удалить с сервера. Из остальных
|
||||
v1-доков этот словарь убран.
|
||||
|
||||
@@ -1326,7 +1475,7 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
|
||||
1. Выпишите с работающего сервера список пиров и их секреты.
|
||||
2. Очистите сервер: `tools/legacy/purge-v0.sh` или ручная процедура из
|
||||
[docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md).
|
||||
[docs/operations/14-legacy-cleanup.md](docs/operations/14-legacy-cleanup.md).
|
||||
3. Разверните `1.0.0` на чистом Debian 13 из release-пакета.
|
||||
4. Заведите пиров заново и раздайте новые клиентские ссылки.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user