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