Files
HY2XS_flamy/fix24.1.txt
T

714 lines
24 KiB
Plaintext
Raw Blame History

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