Удалить временные файлы fix24/fix24.1/fix24.2

This commit is contained in:
2026-05-08 23:17:34 +05:00
parent 1bca73814e
commit 96b71aaa4e
3 changed files with 0 additions and 1573 deletions
-713
View File
@@ -1,713 +0,0 @@
Да, это логичное продолжение: `Account Info` в текущей архитектуре надо не “улучшать”, а удалить как концепцию. После разделения `admin_user` и `peer` эта страница теряет смысл: админ — это оператор панели, а не клиентский peer с квотой, Node URL и QR. Правильная замена — единый стартовый `Dashboard`, куда переезжают системный мониторинг, состояние Hysteria2, агрегированная статистика peer’ов, трафик и security notices.
Ниже — дополнение к плану выше.
---
## 9. Заменить `Account Info` + `System Monitor` на полноценный Dashboard
Сейчас есть две слабые страницы:
`views/info/account/index.vue` — фактически профиль текущего `account`, но там одновременно greeting, create time, quota/download/upload/expire time, subscription buttons, QR и security notifications.
`views/monitor/system/index.vue` — набор карточек без истории: версия панели, CPU, RAM, disk, версия Hysteria2, статус, online users/devices.
После разделения админов и peer’ов это должно стать так:
```text
/dashboard
├─ общая сводка панели
├─ состояние Hysteria2
├─ общий трафик всех peer’ов
├─ графики CPU/RAM/Disk
├─ online peers/devices
├─ top peers by traffic
├─ security warnings
└─ быстрые действия оператора
```
`Account Info` удалить из меню. Профиль админа оставить в dropdown справа сверху: “Profile”, “Change password”, “Logout”. Peer-информация, Node URL и QR должны жить только в `Peer Detail / Peer Drawer`, а не в профиле администратора.
---
## 10. Что именно не так в текущей реализации
Текущий `Account Info` вызывает `getAccountApi({ id: accountStore.id })`. Пока админ и peer лежат в одной таблице, это случайно работает. После нормального разделения это станет архитектурной ошибкой: admin ID не должен использоваться как peer ID.
Кнопки `Subscribe`, `Subscribe QR`, `Node URL`, `Node QR Code` на странице админа надо убрать полностью. Они допустимы только для конкретного peer’а. Сейчас из-за смешанной модели админ может выглядеть как клиент подключения.
`System Monitor` сейчас делает два независимых запроса: `/monitor/monitorSystem` и `/monitor/monitorHysteria2`. Для дашборда это лучше заменить одним агрегирующим endpoint’ом, иначе UI будет собирать бизнес-смысл из разных API.
`MonitorSystem()` каждый раз делает live-запросы к CPU/RAM/Disk. CPU-сэмпл через `cpu.Percent(time.Second, false)` блокирует примерно на секунду. Для интерактивного Dashboard это плохая модель: страницу могут открыть несколько админов, и каждый начнёт дергать системные метрики.
`GetDiskPercent()` берёт первый partition из `disk.Partitions(true)`. Это ненадёжно: первым может оказаться не root/data partition. Для панели нужно мониторить `/`, data-dir панели и, если нужно, mountpoint с Hysteria2 config/logs.
`MonitorHysteria2()` каждый раз выполняет `hysteria2 version`. Версию бинарника не надо получать на каждый render дашборда. Её можно кешировать и обновлять редко: при старте, после смены версии, по ручному refresh или раз в несколько минут.
---
## 11. Новый backend-модуль: `dashboard`
Добавить отдельный модуль, а не расширять `monitor.go`.
```text
controller/dashboard.go
service/dashboard.go
service/metrics_collector.go
dao/dashboard.go
model/vo/dashboard.go
router/dashboard.go
```
Минимальный набор API:
```text
GET /hui/dashboard/summary
GET /hui/dashboard/timeseries?range=1h&step=1m
GET /hui/dashboard/top-peers?range=24h&limit=10
GET /hui/dashboard/security
```
Можно начать с одного endpoint’а:
```text
GET /hui/dashboard
```
Но лучше сразу разделить summary и timeseries. Summary обновляется часто, графики можно обновлять реже.
Целевой response для summary:
```go
type DashboardSummaryVo struct {
CollectedAt int64 `json:"collectedAt"`
System SystemSummaryVo `json:"system"`
Hysteria HysteriaSummaryVo `json:"hysteria"`
Peers PeerSummaryVo `json:"peers"`
Traffic TrafficSummaryVo `json:"traffic"`
SecurityRisks []SecurityRiskVo `json:"securityRisks"`
}
type SystemSummaryVo struct {
CpuPercent float64 `json:"cpuPercent"`
MemUsedBytes uint64 `json:"memUsedBytes"`
MemTotalBytes uint64 `json:"memTotalBytes"`
MemPercent float64 `json:"memPercent"`
DiskUsedBytes uint64 `json:"diskUsedBytes"`
DiskTotalBytes uint64 `json:"diskTotalBytes"`
DiskPercent float64 `json:"diskPercent"`
UptimeSeconds uint64 `json:"uptimeSeconds"`
Load1 float64 `json:"load1"`
}
type HysteriaSummaryVo struct {
Version string `json:"version"`
Running bool `json:"running"`
ApiReachable bool `json:"apiReachable"`
LastApiError string `json:"lastApiError,omitempty"`
}
type PeerSummaryVo struct {
Total int64 `json:"total"`
Enabled int64 `json:"enabled"`
Disabled int64 `json:"disabled"`
Expired int64 `json:"expired"`
OnlinePeers int64 `json:"onlinePeers"`
OnlineDevices int64 `json:"onlineDevices"`
}
type TrafficSummaryVo struct {
DownloadBytes int64 `json:"downloadBytes"`
UploadBytes int64 `json:"uploadBytes"`
TotalBytes int64 `json:"totalBytes"`
TodayDownloadBytes int64 `json:"todayDownloadBytes"`
TodayUploadBytes int64 `json:"todayUploadBytes"`
SinceResetDownloadBytes int64 `json:"sinceResetDownloadBytes"`
SinceResetUploadBytes int64 `json:"sinceResetUploadBytes"`
}
```
Важно: после разделения таблиц все peer-агрегации идут только из `peer`, не из `admin_user`.
---
## 12. Отдельный collector вместо live-сбора на каждый HTTP-запрос
Dashboard не должен каждый раз сам опрашивать систему и Hysteria2 API. Нужен background collector.
```text
MetricsCollector
├─ каждые 5–10 секунд: CPU/RAM/load/uptime
├─ каждые 30–60 секунд: disk
├─ каждые HYSTERIA2_TRAFFIC_TIME секунд: Hysteria2 traffic
├─ каждые 5–15 секунд: Hysteria2 online
└─ редко: Hysteria2 version
```
HTTP endpoint читает готовый snapshot из памяти и, если нужно, последние точки из SQLite. Это решает сразу три проблемы: быстрый UI, меньше нагрузки, стабильная история для графиков.
Пример внутренней модели:
```go
type MetricsSnapshot struct {
CollectedAt time.Time
System SystemMetrics
Hysteria HysteriaRuntimeMetrics
Online map[string]int64
}
```
Для защиты от зависаний все внешние операции — с timeout и context. Hysteria2 API уже вызывается с timeout, это надо сохранить.
---
## 13. Исправить учёт трафика перед графиками
Это критично.
В текущем `saveAccountTraffic()` берётся `/traffic?clear=1`, после чего `rx/tx` умножаются на `HYSTERIA2_TRAFFIC_TIME`. По документации Hysteria2 `/traffic` возвращает traffic statistics по клиентам, а параметр `clear=1` обнуляет статистику после возврата. То есть эти значения надо трактовать как дельту с прошлого clear, а не как rate, который нужно умножать на интервал. Иначе трафик будет завышаться. ([v2.hysteria.network][1])
Нормальный pipeline:
```text
1. collector вызывает /traffic?clear=1
2. получает rx/tx bytes по auth/client id
3. сохраняет immutable traffic_sample
4. атомарно инкрементит peer.download_bytes / peer.upload_bytes
5. обновляет aggregate таблицы для dashboard
```
Новая таблица:
```sql
CREATE TABLE traffic_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
peer_id INTEGER NOT NULL,
auth_id TEXT NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
sampled_at INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_traffic_sample_time ON traffic_sample(sampled_at);
CREATE INDEX idx_traffic_sample_peer_time ON traffic_sample(peer_id, sampled_at);
```
Для быстрых графиков и top peers добавить агрегаты:
```sql
CREATE TABLE traffic_aggregate_hourly (
peer_id INTEGER NOT NULL,
hour_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(peer_id, hour_start)
);
CREATE TABLE traffic_aggregate_daily (
peer_id INTEGER NOT NULL,
day_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(peer_id, day_start)
);
```
Сброс трафика peer’а не должен удалять историю. Он должен сбрасывать только счётчики quota-period:
```text
peer.download_bytes = 0
peer.upload_bytes = 0
peer.traffic_reset_at = now
```
Иначе после reset dashboard потеряет исторические графики. В UI надо явно различать:
```text
Traffic since reset
Traffic today
Traffic last 24h
Traffic total tracked
```
---
## 14. Online users/devices: переименовать и использовать корректно
Hysteria2 `/online` возвращает map client ID → количество client instances. В документации отдельно указано, что это именно количество клиентских экземпляров, то есть “devices”, а не количество активных proxy-соединений. ([v2.hysteria.network][1])
Поэтому в Dashboard надо писать:
```text
Online peers
Online devices
```
Не “online users”, если в новой модели сущность называется `peer`.
Расчёт:
```text
onlinePeers = len(onlineMap)
onlineDevices = sum(onlineMap values)
```
После миграции ключом должен быть `peer.auth_id` или `peer.name`, но не admin username. Главное — не завязываться на таблицу админов.
---
## 15. Security warnings перенести из уведомлений в Dashboard
Сейчас `Account Info` показывает всплывающие `ElNotification`: default password и no HTTPS. Это раздражает, перекрывает интерфейс и не масштабируется.
На Dashboard сделать постоянный блок сверху:
```text
Security Center
⚠ Default admin password / force password change
⚠ Panel is served without HTTPS
⚠ Hysteria2 Traffic Stats API is not protected
⚠ Hysteria2 is stopped
⚠ Public endpoint env is not configured
```
Для Traffic Stats API это особенно важно: Hysteria2 docs прямо рекомендуют задавать `secret`, иначе любой, кто имеет доступ к API listen address, сможет смотреть traffic stats и kick users. ([v2.hysteria.network][2])
Backend должен отдавать structured warnings:
```go
type SecurityRiskVo struct {
Key string `json:"key"` // dashboard.security.noHttps
Severity string `json:"severity"` // info | warning | critical
ActionRoute string `json:"actionRoute,omitempty"`
Dismissible bool `json:"dismissible"`
}
```
Frontend переводит `key` через i18n. Никаких HTML-строк из backend. Сейчас `dangerouslyUseHTMLString` лучше убрать.
---
## 16. Новый frontend layout Dashboard
Создать:
```text
src/views/dashboard/index.vue
src/api/dashboard/index.ts
src/api/dashboard/types.ts
src/views/dashboard/components/MetricCard.vue
src/views/dashboard/components/SecurityAlerts.vue
src/views/dashboard/components/SystemChart.vue
src/views/dashboard/components/TrafficChart.vue
src/views/dashboard/components/TopPeers.vue
src/views/dashboard/components/HysteriaStatus.vue
```
Структура страницы:
```text
[Security alerts, если есть]
[Hysteria2 status] [Online peers] [Online devices] [Traffic today]
[CPU] [Memory] [Disk] [Total peers]
[Traffic chart: download/upload]
[System chart: CPU/RAM]
[Top peers by traffic]
[Recent peer activity / last connections]
```
Первый экран должен отвечать на вопросы оператора:
```text
Сервис работает?
Есть ли пользователи онлайн?
Сколько трафика прошло?
Есть ли перегруз CPU/RAM/Disk?
Кто больше всего потребляет?
Есть ли security/config warnings?
```
То, что сейчас на `System Monitor`, можно сохранить как часть Dashboard, но карточки надо сделать полезнее:
```text
CPU: 14.2%, load1 0.23
Memory: 1.2 GB / 13.7 GB, 8.7%
Disk: 11.5 GB / 40 GB, 28.8%
Hysteria2: Running, v2.8.2
```
Не только проценты.
---
## 17. Графики: как сделать без костылей
В проекте сейчас нет chart-библиотеки. Есть два нормальных варианта.
Вариант A, практичный: добавить ECharts. Для админ-панелей это стандартное решение: line/area charts, tooltip, resize, нормальная работа с time-series. Компоненты графиков лучше lazy-load’ить, чтобы не раздувать initial bundle.
Вариант B, минимальный: написать `Sparkline.vue` на SVG для CPU/RAM и traffic. Меньше зависимостей, но хуже tooltip, zoom, диапазоны и легенды.
Для твоего кейса я бы выбрал ECharts, но не тащил бы его во все страницы. Только Dashboard chunk.
Графики:
```text
TrafficChart:
series: download, upload
ranges: 1h / 24h / 7d / 30d
source: traffic_aggregate_hourly/daily
SystemChart:
series: cpuPercent, memPercent
ranges: 1h / 6h / 24h
source: metric_sample
```
Disk не нужно рисовать как частый line chart. Disk меняется медленно; достаточно карточки и, максимум, daily trend.
---
## 18. Таблица системных метрик
Добавить таблицу:
```sql
CREATE TABLE metric_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sampled_at INTEGER NOT NULL,
cpu_percent REAL NOT NULL DEFAULT 0,
load1 REAL NOT NULL DEFAULT 0,
mem_used_bytes INTEGER NOT NULL DEFAULT 0,
mem_total_bytes INTEGER NOT NULL DEFAULT 0,
mem_percent REAL NOT NULL DEFAULT 0,
disk_path TEXT NOT NULL DEFAULT '/',
disk_used_bytes INTEGER NOT NULL DEFAULT 0,
disk_total_bytes INTEGER NOT NULL DEFAULT 0,
disk_percent REAL NOT NULL DEFAULT 0,
hysteria_running INTEGER NOT NULL DEFAULT 0,
online_peers INTEGER NOT NULL DEFAULT 0,
online_devices INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_metric_sample_time ON metric_sample(sampled_at);
```
Retention:
```text
raw metric_sample: 2472 часа
minute aggregate: 7 дней
hour aggregate: 3090 дней
daily aggregate: можно хранить дольше
```
SQLite нормально выдержит такие объёмы, если не писать каждую секунду и не хранить бесконечную raw-историю.
---
## 19. Роутинг и меню
Текущее:
```text
/ -> /info/account
/info/account
/monitor/system
```
Целевое:
```text
/ -> /dashboard
/dashboard
/peers
/hysteria
/config
/log/system
/log/hysteria
```
В `router/index.ts`:
```ts
{
path: "/",
component: Layout,
redirect: "/dashboard",
children: [...]
}
```
Новый route:
```ts
{
path: "/dashboard",
component: "Layout",
redirect: "/dashboard/index",
name: "Dashboard",
meta: {
title: "dashboard",
icon: "dashboard",
roles: ["admin"],
},
children: [
{
path: "index",
component: "dashboard/index",
name: "DashboardIndex",
meta: {
title: "dashboard",
icon: "dashboard",
roles: ["admin"],
},
},
],
}
```
`Info` route удалить. `Monitor` route либо удалить, либо временно оставить redirect на `/dashboard/index` на один релиз.
---
## 20. Что делать с бывшим `Account Info`
Разложить по новым местам.
Greeting и create time админа:
```text
navbar dropdown / admin profile
```
Security warnings:
```text
dashboard Security Center
```
Quota/download/upload/expire:
```text
peer drawer
peer list compact cards
dashboard aggregate traffic
```
Subscribe/QR/Node URL:
```text
peer detail только для конкретного peer
```
Default password warning:
```text
dashboard + forced change-password flow
```
HTTPS warning:
```text
dashboard security warning
```
Так исчезает главный conceptual bug: админ больше не выглядит как peer.
---
## 21. Быстрые действия на Dashboard
Добавить только operator actions, не peer actions:
```text
Restart Hysteria2
Open Hysteria config
Open peers
Open logs
Refresh metrics
```
Не добавлять на dashboard “Create peer” как главную кнопку, если страница перегружена. Лучше маленькая secondary action в блоке peers.
Для dangerous actions — только confirm dialog:
```text
Restart Hysteria2
Reset all traffic
```
`Reset all traffic` я бы вообще не держал на Dashboard. Это административная операция, ей место в settings или peer management.
---
## 22. Backend queries для агрегатов peer’ов
После split-модели нужны DAO-методы:
```go
CountPeersByStatus(now int64) (PeerSummary, error)
SumPeerTrafficSinceReset() (TrafficSummary, error)
TopPeersByTraffic(from, to int64, limit int) ([]TopPeerVo, error)
ListRecentPeerActivity(limit int) ([]PeerActivityVo, error)
```
SQL-логика:
```sql
SELECT
COUNT(*) AS total,
SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) AS enabled,
SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) AS disabled,
SUM(CASE WHEN expires_at > 0 AND expires_at < ? THEN 1 ELSE 0 END) AS expired
FROM peer;
```
Traffic current period:
```sql
SELECT
COALESCE(SUM(download_bytes), 0) AS download,
COALESCE(SUM(upload_bytes), 0) AS upload
FROM peer
WHERE disabled = 0;
```
Top peers из history:
```sql
SELECT
peer_id,
SUM(rx_bytes) AS download,
SUM(tx_bytes) AS upload
FROM traffic_sample
WHERE sampled_at BETWEEN ? AND ?
GROUP BY peer_id
ORDER BY download + upload DESC
LIMIT ?;
```
---
## 23. Polling на frontend
Первый production-safe вариант без WebSocket/SSE:
```text
summary: каждые 5 секунд
timeseries: каждые 30 секунд
top peers: каждые 60 секунд
```
Использовать `useIntervalFn` из `@vueuse/core`, он уже есть в dependencies.
Обязательно:
```text
pause polling on unmount
pause polling when tab hidden
show stale badge if collectedAt older than N seconds
manual refresh button
```
Если позже захочешь realtime без polling — добавить SSE:
```text
GET /hui/dashboard/events
```
Но я бы не начинал с SSE. Для такой панели polling проще, надёжнее и легче тестируется.
---
## 24. Состояния ошибок
Dashboard должен показывать не просто `-`, а причину.
Примеры:
```text
Hysteria2 stopped
Traffic API unreachable
Traffic API unauthorized
No traffic samples yet
Metrics collector stale
Disk path unavailable
```
Backend response:
```go
type DataHealthVo struct {
Status string `json:"status"` // ok | stale | error
MessageKey string `json:"messageKey,omitempty"`
LastSuccessAt int64 `json:"lastSuccessAt,omitempty"`
}
```
Например:
```json
{
"hysteria": {
"running": true,
"apiReachable": false,
"lastApiError": "dashboard.error.trafficApiUnauthorized"
}
}
```
Frontend переводит через i18n.
---
## 25. Acceptance criteria для Dashboard
Готовность можно проверять так:
```text
/ открывает /dashboard, а не /info/account.
В меню нет Account Info.
Admin profile не содержит quota, Node URL, Subscribe, QR.
Dashboard показывает Hysteria2 status, version, online peers/devices.
Dashboard показывает общий download/upload всех peer’ов.
Dashboard показывает traffic today и traffic since reset.
Dashboard показывает CPU/RAM/Disk не только процентом, но и used/total.
CPU/RAM графики строятся из history, а не из разовых live-запросов.
Traffic график не ломается после reset traffic.
Traffic accounting не умножает /traffic?clear=1 на polling interval.
Security warnings отображаются внутри Dashboard, не всплывающими HTML notification.
Если Hysteria2 API недоступен, Dashboard показывает degraded state, а не пустые карточки.
На 1366px первый экран читается без горизонтального скролла.
```
---
## 26. Приоритет внедрения
Я бы добавил этот блок в общий roadmap так:
```text
A. Security hotfix текущей account/peer модели.
B. Split admin_user / peer.
C. Исправить traffic accounting: убрать ошибочное умножение, добавить traffic_sample.
D. Создать Dashboard backend summary endpoint.
E. Перенести /info/account и /monitor/system в /dashboard.
F. Добавить графики и history retention.
G. Удалить legacy routes /info/account и /monitor/system.
```
Главное: не делать красивый Dashboard поверх старой таблицы `account`. Сначала развести admin и peer, затем строить дашборд на правильной доменной модели. Иначе ты просто перенесёшь старую путаницу в более красивый интерфейс.
[1]: https://v2.hysteria.network/docs/advanced/Traffic-Stats-API/?utm_source=chatgpt.com "Traffic Stats API"
[2]: https://v2.hysteria.network/docs/advanced/Full-Server-Config/?utm_source=chatgpt.com "Full Server Config"
-243
View File
@@ -1,243 +0,0 @@
Да, я бы убирал подписки полностью. В текущем состоянии это не недоделанная мелочь, а мёртвый функциональный слой, который создаёт ложное ожидание у оператора и путает UX.
По текущему коду видно следующее: ordinary Node URL / Node QR строятся отдельно и полезны; subscription-кнопки в UI есть, но backend endpoint `Hysteria2SubscribeUrl` фактически возвращает ошибку `subscription delivery is out of scope in HY2XS baseline`. При этом в service ещё лежит старая логика генерации подписок для Clash/Shadowrocket/v2rayN, но она не подключена нормальным публичным delivery route. То есть это уже рудимент: фронт показывает кнопки, backend говорит “не поддерживается”, а пользователи всё равно подключаются через обычный URI/QR.
Я бы добавил в общий план отдельный пункт.
---
## 27. Удалить subscription layer как неподдерживаемый рудимент
### Решение
Оставить только:
```text
Node URL
Node QR Code
Copy URI
Rotate Secret
```
Удалить:
```text
Subscribe
Subscribe QR Code
Subscription URL
Clash subscription extension
Shadowrocket/V2rayN subscription delivery
/hysteria2/hysteria2SubscribeUrl
```
То есть один peer — один обычный Hysteria2 URI/QR. Этого достаточно для текущей панели, особенно если цель — упрощённая, зрелая 3x-ui-like админка, а не subscription gateway.
---
## 28. Почему лучше удалить, а не чинить сейчас
Подписка — это отдельный продуктовый слой, а не просто “ещё один QR”.
Чтобы она была production-grade, нужны:
```text
публичный endpoint доставки подписки;
форматы под разные клиенты;
нормальная авторизация или signed token;
TTL / revoke / rotate;
rate limit;
логирование выдачи;
обработка client type;
совместимость Clash/Shadowrocket/v2rayN;
безопасное сокрытие peer secret;
корректные HTTP headers;
контроль доступа после disable/expire peer;
тесты на каждый формат клиента.
```
Сейчас этого нет. Более того, подписочный URL по старой логике строится вокруг `conPass`, то есть секрет peer’а становится частью URL. Это хуже обычного QR, потому что URL может попасть в browser history, reverse-proxy logs, access logs, Referer, скриншоты и т.д.
Обычный Node QR тоже содержит секрет, но он показывается авторизованному админу в панели для конкретного peer’а. Подписка же предполагает публичную доставку конфигурации по ссылке. Это другой threat model.
Поэтому чинить подписки сейчас — не “доделать кнопку”, а открывать отдельный блок безопасности и совместимости. Для текущего scope это лишнее.
---
## 29. Что удалить на backend
Удалить или пометить deprecated с последующим удалением:
```go
controller.Hysteria2SubscribeUrl
controller.Hysteria2Subscribe
service.Hysteria2SubscribeUrl
service.Hysteria2Subscribe
dto.Hysteria2SubscribeUrlDto
vo.Hysteria2SubscribeVo
```
Из router убрать:
```go
hysteria2.GET("/hysteria2SubscribeUrl", controller.Hysteria2SubscribeUrl)
```
Если будешь делать новую REST-модель после разделения `admin_user` / `peer`, оставить только:
```text
GET /hui/peers/:id/client-uri
GET /hui/peers/:id/client-qr
```
Или один endpoint:
```text
GET /hui/peers/:id/client-config
```
Response:
```json
{
"uri": "hysteria2://...",
"qrCode": "base64..."
}
```
Важно: этот endpoint должен работать только под admin JWT. Никакой публичной выдачи по `conPass`.
---
## 30. Что удалить на frontend
Из `src/views/account/list/index.vue` убрать:
```text
Subscribe
```
Из будущего peer drawer убрать:
```text
Subscribe
Subscribe QR
```
Из бывшего `src/views/info/account/index.vue` это всё всё равно исчезнет вместе с самой страницей `Account Info`.
Из `src/api/hysteria2/index.ts` убрать:
```ts
hysteria2SubscribeUrlApi
```
Из `src/api/hysteria2/types.ts` убрать:
```ts
Hysteria2SubscribeUrlDto
Hysteria2SubscribeVo
```
Из i18n убрать или оставить deprecated на один релиз:
```text
common.subscribe
common.subscribeQrCode
config.clashExtension
```
Если `CLASH_EXTENSION` используется только для подписки, убрать его из UI настроек. В базе можно не удалять сразу, чтобы не ломать существующие SQLite-файлы. Лучше сделать миграцию так:
```text
релиз N: поле скрыто, backend не использует;
релиз N+1: config key удаляется или игнорируется окончательно.
```
---
## 31. Что оставить вместо подписки
В peer list / peer drawer оставить понятные действия:
```text
Copy URI
Show QR
Rotate Secret
Edit
Reset Traffic
Kick
Disable
Delete
```
В detail drawer можно сделать блок:
```text
Connection
URI: hysteria2://...
[Copy]
[Show QR]
[Rotate secret]
```
И короткий warning:
```text
Rotating secret invalidates existing client configs.
```
Этого достаточно. Оператор создаёт peer, копирует URI или QR, отдаёт клиенту. Без псевдоподписок.
---
## 32. Если когда-нибудь возвращать подписки
Возвращать их стоит только как отдельную feature, не как восстановление старого кода.
Нормальная модель:
```text
subscription_token
id
peer_id
token_digest
expires_at
revoked_at
created_at
last_used_at
```
Публичный endpoint:
```text
GET /sub/:token
```
Token должен быть random, revocable, не равен peer secret. В URL нельзя класть `conPass`.
Формат:
```text
GET /sub/:token?target=clash
GET /sub/:token?target=shadowrocket
GET /sub/:token?target=v2rayn
```
Но я бы сейчас этого не делал. Это увеличит scope, а ценность низкая, если обычные URI/QR уже закрывают основной сценарий.
---
## 33. Acceptance criteria для удаления подписок
```text
В UI нигде нет Subscribe / Subscribe QR.
В peer list нет действий, ведущих к subscription endpoint.
В API клиента нет hysteria2SubscribeUrlApi.
Backend router не регистрирует /hysteria2/hysteria2SubscribeUrl.
Service-код генерации подписок удалён или явно deprecated и недоступен.
Обычный Node URL и Node QR продолжают работать.
Админ не получает Node URL/QR для себя, только для peer’ов.
В настройках нет Clash subscription extension, если подписки отключены.
```
Итог: да, подписку лучше удалить. Не скрыть кнопку CSS’ом, не оставить endpoint с `Fail`, а именно вырезать из публичного UI/API. Это упростит продукт, уберёт ложные кнопки и снизит риск утечки peer secret через URL.
-617
View File
@@ -1,617 +0,0 @@
Посмотрел архив `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 до 240248px.
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. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.