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