Удалить временные файлы fix24/fix24.1/fix24.2
This commit is contained in:
-713
@@ -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: 24–72 часа
|
|
||||||
minute aggregate: 7 дней
|
|
||||||
hour aggregate: 30–90 дней
|
|
||||||
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
@@ -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.
|
|
||||||
@@ -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 до 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. После этого локаль и таблица чинятся уже как нормальная фронтенд-задача, а не как борьба с последствиями смешанной модели.
|
|
||||||
Reference in New Issue
Block a user