Удалить временные файлы 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