Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа

This commit is contained in:
2026-05-08 23:16:25 +05:00
parent de4a65a959
commit 1bca73814e
85 changed files with 4119 additions and 1911 deletions
+713
View File
@@ -0,0 +1,713 @@
Да, это логичное продолжение: `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"