Посмотрел архив `HY2XS_flamy-main.zip`. Проблема не косметическая: сейчас в проекте смешаны две разные доменные сущности — администратор панели и peer Hysteria2. Это видно прямо по схеме `account`: в одной таблице лежат `username/pass` для входа в админку, `con_pass` для подключения, `quota/download/upload/device_no`, `role`, `deleted`. Логин панели фильтрует `role = 'admin'`, а Hysteria2 auth ищет только по `con_pass` и не отсекает `role = 'admin'`. Поэтому админ технически остаётся peer’ом. Ниже план нормального продакшен-исправления без «подмазать CSS и добавить if role != admin». --- ## 1. Развести админов панели и peer’ов Hysteria2 ### Текущее состояние Сейчас: `account.pass` — пароль входа в панель. `account.con_pass` — пароль подключения Hysteria2. `account.role` — попытка различать `admin/user`. `account.quota/download/upload/device_no/expire_time` — peer-поля. `login_at` — поле админа. `con_at` — поле peer’а. Это плохая модель. Роль не должна превращать одну таблицу в две разные сущности. ### Целевая модель Сделать минимум две таблицы. `admin_user`: ```sql id username password_hash status force_password_change last_login_at password_changed_at token_version created_at updated_at ``` `peer`: ```sql id name remark auth_id secret_digest secret_ciphertext -- если нужно показывать URL/QR после создания quota_bytes download_bytes upload_bytes expires_at max_devices disabled banned_until last_connection_at created_at updated_at ``` `admin_user` не должен иметь `quota`, `con_pass`, `device_no`, `download`, `upload`. `peer` не должен иметь пароль входа в панель и `role`. ### Как хранить peer secret Текущий `con_pass` хранится как plain text. Для продакшена лучше уйти от этого. Вариант нормальный: `auth_id` — публичный идентификатор peer’а, например короткий random/base32. `raw_secret` — генерируется при создании или ротации. `secret_digest = HMAC-SHA256(raw_secret, HY2XS_PEER_SECRET_KEY)` — используется для auth lookup. `secret_ciphertext` — опционально, если UI должен уметь повторно показать Node URL/QR. Шифровать ключом из data-dir/env, не хранить просто строкой в SQLite. Если не хочется вводить шифрование сейчас, допустим компромисс: хранить `secret_plain` временно, но уже в таблице `peer`, не рядом с admin password. Потом отдельной миграцией заменить на digest/encrypted secret. ### Миграция Сделать версионированные миграции, а не держать огромную строку SQL внутри `dao/sqlite.go`. Сейчас есть два источника схемы: `apps/docs/sql/h_ui_db.sql` и inline `sqlInitStr` в `apps/dao/sqlite.go`. Они уже расходятся: в `sqlite.go` добавлен `force_password_change`, в SQL-доке его нет; `remark` объявлен как `INTEGER DEFAULT ''`, хотя в Go это `string`. Это надо убрать. Нужен один механизм: ```sql schema_migrations(version, applied_at) ``` Миграции: `001_initial_legacy_snapshot.sql` — текущая схема, только для reference. `002_admin_peer_split.sql` — создаёт `admin_user`, `peer`. `003_migrate_legacy_accounts.sql` — переносит данные. `004_drop_or_archive_legacy_account.sql` — не сразу удалять, а переименовать в `legacy_account_backup`. Правила переноса: `role = 'admin'` → `admin_user`. Переносить `username`, `pass`, `force_password_change`, `login_at`. Не переносить `con_pass`. `role != 'admin'` → `peer`. Переносить `username` как `name`, `remark`, лимиты, трафик, `expire_time`, `kick_util_time`, `con_at`. После миграции admin credentials больше не могут пройти Hysteria2 auth даже теоретически. --- ## 2. Переписать backend-слой по доменам ### Что заменить Сейчас всё сидит в `account.go`: DAO, service, controller, DTO, VO. Надо разделить: ```text dao/admin_user.go dao/peer.go service/auth.go service/admin_user.go service/peer.go service/hysteria2_auth.go controller/auth.go controller/admin_user.go controller/peer.go controller/hysteria2.go ``` ### API Оставить `/hui/auth/login`, но он должен работать только с `admin_user`. Добавить: ```text GET /hui/admin/me POST /hui/admin/change-password GET /hui/peers POST /hui/peers GET /hui/peers/:id PATCH /hui/peers/:id DELETE /hui/peers/:id POST /hui/peers/:id/reset-traffic POST /hui/peers/:id/kick POST /hui/peers/:id/release-kick POST /hui/peers/:id/rotate-secret GET /hui/peers/:id/client-url GET /hui/peers/:id/qr ``` Старые `/account/*` можно оставить только как compatibility layer на один релиз, но UI уже должен ходить в `/peers/*`. ### Hysteria2 auth Сейчас `Hysteria2Auth()` делает: ```go dao.GetAccount("con_pass = ? and deleted = 0 ...") ``` Нужно заменить на peer-auth: ```go peer, err := peerRepo.FindBySecretDigest(digest) ``` И проверять только peer-поля: ```text disabled = false now < expires_at quota_bytes < 0 OR quota_bytes > download_bytes + upload_bytes now > banned_until online_devices < max_devices ``` Admin-таблица здесь вообще не импортируется. ### Сессии и JWT JWT сейчас содержит `AccountBo` с `Roles`. Оставить можно, но лучше переименовать в `AdminClaims`. Добавить `token_version` в `admin_user`. Тогда смена пароля, reset или принудительная инвалидизация токенов делается увеличением `token_version`. После смены пароля обязательно сбрасывать `force_password_change = 0`. Сейчас флаг возвращается из логина, но нормального dedicated flow для смены admin password не видно. ### Reset command `apps/cmd/reset.go` сейчас меняет row `id=1` в `account`, печатает ещё и `Connection Password`. После разделения: ```text hy2xs-admin reset-admin ``` Должен менять только `admin_user`. Никакого connection password для админа печатать нельзя. --- ## 3. UI: переименовать “Account Manage” в peer management В интерфейсе сейчас название “Account” вводит в заблуждение. Там смешаны профиль админа и peer’ы. Нужна терминология: ```text Профиль / Admin Profile Пиры / Peers Управление пирами / Peer Management ``` В форме создания peer убрать поле “Пароль входа”. Для peer нужен только generated/rotatable connection secret. Нормальная форма создания peer: ```text Комментарий Имя / label Квота Срок действия Лимит устройств Статус [Создать] ``` После создания показать одноразовый блок: ```text Connection URI QR Copy Сохраните сейчас: после закрытия секрет может быть скрыт ``` Если оставляете encrypted secret, можно показывать URL и позже. --- ## 4. Починить русскую локаль и “плывущие” иконки правильно ### Причина Это не проблема русского языка как такового. Русские строки длиннее, а sidebar сейчас завязан на дефолтный layout Element Plus плюс ручные margin’ы: ```scss .svg-icon { margin-right: 16px; } .hideSidebar .el-sub-menu__title { padding: 0 !important; } .hideSidebar .svg-icon { margin-left: 20px; } ``` Из-за этого при длинных заголовках, collapse/open state и sub-menu иконки начинают жить отдельно от текста. ### Исправление В `SidebarItem.vue` обернуть title в отдельный span: ```vue {{ translateRouteTitleI18n(...) }} ``` Для `el-menu-item` и `el-sub-menu__title` задать один стабильный layout: ```scss .sidebar-container { .el-menu-item, .el-sub-menu__title { display: flex; align-items: center; gap: 12px; height: 48px; line-height: normal; padding: 0 16px !important; } .svg-icon { flex: 0 0 18px; width: 18px; height: 18px; margin-right: 0; } .menu-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .el-sub-menu__icon-arrow { margin-left: auto; } } ``` Убрать ручные `margin-left: 20px` для collapsed state. Для collapsed меню текст скрывать штатно, а не через плавающие отступы. Ширину sidebar лучше поднять с `210px` до `240px` или `248px`. Но это вторично. Основной фикс — стабильная flex/grid-разметка. Для длинных пунктов включить tooltip с полным названием при hover. Не переносить текст на вторую строку внутри sidebar. --- ## 5. Перевести уведомления и ошибки без хардкода Сейчас часть UI переведена через `$t`, но много строк осталось захардкоженными: ```text "Required" "Username format is incorrect" "Are you sure to reset traffic?" "Are you sure to delete..." "Warning" "A week later" "file format not supported" ``` Плюс backend возвращает английские строки: ```text wrong password system error permission denied username already exists admin cannot be deleted ``` ### Frontend Добавить namespace: ```ts validation: { required, usernameInvalid, passwordInvalid, peerSecretInvalid, integer, number, fileFormat, fileTooLarge, } confirm: { warning, deletePeer, resetTraffic, restartPanel, } timeShortcut: { hourLater, dayLater, weekLater, monthLater, yearLater, } ``` Все validation rules сделать через `computed`, чтобы при смене языка сообщения обновлялись: ```ts const dataFormRules = computed(() => ({ username: [ { required: true, message: t("validation.required"), trigger: ["blur"] }, ], })) ``` Подключить `ElConfigProvider` в `App.vue` и прокидывать locale Element Plus: ```vue ``` Иначе встроенные компоненты Element Plus, datepicker, pagination и popconfirm будут жить своей локалью. ### Backend Не возвращать UI-текст как источник истины. Возвращать стабильный error code/message key: ```json { "code": 40010, "type": "no", "message": "peer.usernameAlreadyExists", "params": { "username": "testuser" } } ``` Frontend переводит `message` как i18n key. Если key неизвестен — fallback на `common.systemError`. Для machine endpoint `/hysteria2/auth` локализация не нужна, там протокольный ответ `{ ok: true/false, id }`. --- ## 6. Сжать таблицу peer’ов до production-вида Сейчас таблица перегружена. В `account/list/index.vue` одновременно выводятся: ```text ID Remark Username Role Quota Download Upload Online status Online devices Device limit Offline remaining time Expire time Last login time Last connection time Create time Status Operate ``` Для peer list это слишком много. На широком экране оно всё равно не будет хорошо читаться. ### Основной список Оставить в таблице только: ```text Peer Status Traffic Devices Expires Last connection Actions ``` Где: `Peer` — имя + remark + ID мелким текстом. `Status` — enabled/disabled + online/offline. `Traffic` — progress bar: used / quota, а upload/download спрятать в details. `Devices` — online / max. `Expires` — дата + “expired soon/expired” tag. `Last connection` — дата или `-`. `Actions` — 1–2 основные кнопки и dropdown. ### Details drawer / overview dialog По клику “Обзор” открыть drawer: ```text Peer overview - ID - Name - Remark - Created at - Updated at - Download - Upload - Quota - Expires at - Last connection - Kick until - Device limit - Current online devices - Node URL - QR ``` Это лучше, чем прятать половину в троеточие без структуры. ### Actions В таблице оставить: ```text Copy URL Edit ⋯ ``` В dropdown: ```text Show QR Reset traffic Kick Release kick Rotate secret Disable / Enable Delete ``` `Subscribe` сейчас в baseline отключён, значит в UI его лучше не показывать, пока delivery layer вне scope. Иначе оператор видит кнопку, которая всегда ведёт к ошибке. ### Поведение на малых экранах На desktop можно оставить `el-table`. На tablet/mobile лучше отдельный card layout, а не пытаться ужать таблицу. Например: ```text Peer card name / status traffic progress devices expires actions ``` --- ## 7. Конкретный порядок работ ### Этап A. Быстрый security hotfix Это временный фикс, не финальная архитектура. 1. В `Hysteria2Auth` добавить фильтр `role = 'user'`. 2. В `PageAccount` по умолчанию показывать только `role = 'user'`. 3. В `SaveAccount` явно ставить `role = 'user'`. 4. Запретить выдачу Node URL/QR для `role = 'admin'`. 5. В `reset.go` убрать вывод connection password. Это закрывает самый опасный баг до большой миграции. ### Этап B. Нормальная доменная миграция 1. Добавить `schema_migrations`. 2. Создать `admin_user`. 3. Создать `peer`. 4. Перенести legacy data. 5. Переписать DAO/service/controller. 6. Оставить legacy `account` только как backup. 7. Добавить `reset-admin`. 8. Убрать `role` из peer flow. ### Этап C. UI/i18n 1. Переименовать раздел `Account Manage` → `Peer Manage`. 2. Убрать peer login password из формы. 3. Сделать i18n keys для всех validation/confirm/toast строк. 4. Подключить Element Plus locale provider. 5. Убрать backend English strings из UI-отображения. 6. Добавить проверку отсутствующих i18n keys в CI. ### Этап D. Layout/sidebar 1. Переписать sidebar CSS на flex/grid. 2. Убрать ручные margin hacks. 3. Добавить `.menu-title` с ellipsis. 4. Поднять sidebar width до 240–248px. 5. Проверить RU/EN в expanded/collapsed состояниях. ### Этап E. Peer table redesign 1. Сделать компактные колонки. 2. Вынести details в drawer. 3. Перенести второстепенные действия в dropdown. 4. Скрыть subscription actions, если delivery layer отключён. 5. Добавить responsive card layout. --- ## 8. Тесты и критерии готовности Backend tests: ```text admin_user может войти в панель admin_user не может пройти Hysteria2 auth peer не может войти в панель peer может пройти Hysteria2 auth expired peer rejected disabled peer rejected quota-exceeded peer rejected device-limit peer rejected legacy migration переносит admin и peers корректно ``` Frontend checks: ```text pnpm lint:eslint pnpm build:prod vue-tsc --noEmit ``` E2E/screenshot: ```text RU sidebar expanded RU sidebar collapsed EN sidebar expanded EN sidebar collapsed Peer table 1366px Peer table 1920px Peer card/mobile layout ``` Acceptance criteria: ```text Админ панели не отображается в списке peer’ов. У админа нет Node URL, QR и connection password. Peer не имеет password для входа в UI. Все toast/confirm/validation сообщения переводятся. Русская локаль не ломает sidebar. Peer list не требует горизонтального скролла на 1366px. Второстепенные peer-поля доступны через “Обзор”. ``` Главная мысль: не чинить это через `role` и CSS-отступы. Правильный продакшен-фикс — разделить admin identity и peer identity на уровне схемы, сервисов и UI. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.