714 lines
24 KiB
Plaintext
714 lines
24 KiB
Plaintext
Да, это логичное продолжение: `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"
|