fix(admin): достроить вторые половины отзыва доступа, лимита и журнала
Разбор кода на c0a43ae со сверкой с официальной документацией Hysteria 2.
Общая тема: операции, у которых была только одна из двух необходимых половин.
Отзыв доступа. Запись disabled=1 видит лишь выборка в Hysteria2Auth, то есть
закрывает БУДУЩИЕ обращения к HTTP-auth; установленная QUIC-сессия живёт своей
жизнью и сама не разрывается. После «Отключить» пир пользовался доступом сколько
угодно долго, а панель показывала его отключённым. Появился DisconnectPeers —
только официальный Traffic Stats /kick, без записи в базу; прежний Hysteria2Kick
вместе с разрывом проставлял banned_until и потому для отключения не годился.
Порядок «запись, затем разрыв» обратному не подлежит и доказан снимком базы в
момент прихода /kick. Неудача разрыва не откатывает disabled и сообщается кодом
peer_disconnect_failed: обычная ошибка означала бы для оператора вывод, прямо
противоположный истине. KickPeer переведён на тот же примитив — он писал
banned_until дважды и мог ответить чистым отказом уже в применённом состоянии.
Ограничение устройств. Отказ /online обрабатывался возвратом успеха
авторизации, то есть недоступность 127.0.0.1 превращала объявленный лимит в
безлимит. Вторая половина дыры была тише: Hysteria2Online отдавал пустую карту
БЕЗ ошибки, когда systemd отвечал «служба неактивна», — а этот ответ не
отличается от «спросить systemctl не удалось». Пути разделены: терпимый для
отображения, строгий для решения о доступе. Hysteria2IsRunning убран с путей
принятия решений совсем.
Журнал. entry.Info() вызывался без аргумента, и logrus писал "msg":"" для
каждого запроса — пустой столбец на экране был точным отражением файла. Ветка
«файла ещё нет» отвечала голым массивом вместо {records,total}, поэтому на
свежей установке страница системных логов не работала вовсе. Битая строка
вызывала vo.Fail И continue: клиент получал два JSON-документа подряд.
Панель. Общий LogViewer и utils/download.ts (копий скачивания было четыре, две
ставили запрос вне try и глушили причину); меню на command с быстрым
включением/отключением; popper-style у подсказки; kick с подтверждением и
названным сроком; отмена подтверждений перестала быть ошибкой. Отдельно:
skipErrorToast гасил и транспортный отказ, при том что страницы писали
«перехватчик уже показал» и молчали, — обрыв связи не показывал ничего.
Закреплено go-тестами против настоящего HTTP, контрактными тестами панели и
двумя гейтами приёмки. Ручная часть — в
docs/acceptance/2026-09-01-v1.0.0-rc2-preflight-findings.md.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Контракты панели
|
||||
|
||||
Три свойства HY2XS admin, которые не проверяются ни типами, ни сборкой bundle и
|
||||
Свойства HY2XS admin, которые не проверяются ни типами, ни сборкой bundle и
|
||||
потому ломались молча. Каждое из них закреплено тестом
|
||||
(`tools/test/frontend-*.test.ts`) и гейтом приёмки.
|
||||
|
||||
@@ -134,7 +134,82 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. Атрибуция
|
||||
## 4. Таблицы журнала
|
||||
|
||||
**Правило.** Колонка журнала объявляет свою ширину: служебные — через `width`,
|
||||
содержательная — через `min-width`.
|
||||
|
||||
Без этого Element Plus делит доступную ширину между колонками практически
|
||||
поровну. У журнала колонок три, поэтому уровень и время получали по трети
|
||||
строки, а сообщение — единственное содержимое журнала — тоже треть.
|
||||
|
||||
**Сообщение переносится, а не обрезается.** У Hysteria в `msg` приезжает
|
||||
диагностический JSON; строка, обрезанная многоточием, не отвечает ни на один
|
||||
вопрос, ради которого страницу открыли.
|
||||
|
||||
**Обе страницы журнала построены на одном компоненте**
|
||||
(`components/LogViewer`). Они были побайтово одинаковы и несли одни и те же три
|
||||
дефекта в двух экземплярах — ширины, обработку отказа выгрузки и форму ответа.
|
||||
Собственная `el-table-column` на странице журнала запрещена гейтом приёмки.
|
||||
|
||||
---
|
||||
|
||||
## 5. Выгрузка файлов
|
||||
|
||||
**Правило.** Сборка ссылки на скачивание существует в панели в единственном
|
||||
экземпляре — `utils/download.ts`. Единственность проверяется контрактным
|
||||
тестом по вхождению `createObjectURL`.
|
||||
|
||||
Копий было четыре, и все успели разойтись. Две из них ставили сетевой запрос
|
||||
ПЕРЕД `try`:
|
||||
|
||||
```ts
|
||||
const response = await exportApi(...); // отказ сюда не попадает
|
||||
try { ... } catch (e) { /* empty */ }
|
||||
```
|
||||
|
||||
то есть отказ самого запроса не ловился вовсе, а всё внутри глушилось молча:
|
||||
оператор не получал ни файла, ни причины. Третья падала на `split(...)` при
|
||||
отсутствующем `Content-Disposition` — и это исключение тоже глушилось.
|
||||
|
||||
**Отказ выгрузки показывает сама страница.** Бинарный ответ не проходит через
|
||||
общий разбор конверта: у `Blob` нет полей `code` и `errors`, поэтому
|
||||
перехватчик по нему фразы не даст.
|
||||
|
||||
---
|
||||
|
||||
## 6. Меню действий над строкой
|
||||
|
||||
**Правило.** Пункты `el-dropdown` объявляют `command`; обработчик — один, на
|
||||
`el-dropdown`.
|
||||
|
||||
`@click` на каждом пункте не запрещён самим Element Plus, но `command` является
|
||||
штатным контрактом именно для меню действий, и при нём невозможно добавить
|
||||
пункт, забыв его подключить. Обе половины проверяются контрактным тестом:
|
||||
наличие `@command` и отсутствие `@click` на пунктах.
|
||||
|
||||
**Частичный результат операции отличается от отказа кодом.** Отзыв доступа к
|
||||
VPN состоит из двух половин — записи в базе и разрыва активной сессии, — и
|
||||
первая может примениться без второй. Панель обязана распознать
|
||||
`peer_disconnect_failed` по коду, показать его предупреждением, а не ошибкой, и
|
||||
ОБНОВИТЬ строку: состояние в базе уже изменилось. Показ его как обычной ошибки
|
||||
подтолкнул бы оператора к выводу, прямо противоположному истине.
|
||||
|
||||
---
|
||||
|
||||
## 7. Подтверждения
|
||||
|
||||
**Правило.** Отмена подтверждения — это ответ оператора, а не ошибка.
|
||||
|
||||
`ElMessageBox` отклоняет промис при нажатии «Отмена». `await
|
||||
ElMessageBox.confirm(...)` без разбора отказа оставляет необработанное
|
||||
отклонение промиса на каждую отмену. Единственный прямой вызов на странице
|
||||
пиров живёт внутри `confirmAction`, переводящей отмену в обычное `false`; это
|
||||
закреплено тестом.
|
||||
|
||||
---
|
||||
|
||||
## 8. Атрибуция
|
||||
|
||||
Адрес атрибуции объявлен один раз в `apps/frontend/src/constants/branding.ts` и
|
||||
принадлежит приложению. Он не является операторской настройкой: ни `hy2xs.env`,
|
||||
|
||||
Reference in New Issue
Block a user