Посмотрел архив `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
<span class="menu-title">
  {{ translateRouteTitleI18n(...) }}
</span>
```

Для `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
<el-config-provider :locale="elementLocale">
  <router-view />
</el-config-provider>
```

Иначе встроенные компоненты 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. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.
